2168 lines
90 KiB
Python
2168 lines
90 KiB
Python
"""The main window of the ComicTagger app"""
|
|
#
|
|
# Copyright 2012-2014 ComicTagger Authors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import operator
|
|
import os
|
|
import pickle
|
|
import platform
|
|
import pprint
|
|
import re
|
|
import sys
|
|
import webbrowser
|
|
from collections.abc import Iterable
|
|
from datetime import datetime
|
|
from typing import Any, Callable
|
|
from urllib.parse import urlparse
|
|
|
|
import natsort
|
|
import settngs
|
|
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
|
|
|
|
from comicapi import utils
|
|
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
|
from comicapi.comicinfoxml import ComicInfoXml
|
|
from comicapi.filenameparser import FileNameParser
|
|
from comicapi.genericmetadata import GenericMetadata
|
|
from comicapi.issuestring import IssueString
|
|
from comictaggerlib import ctversion
|
|
from comictaggerlib.applicationlogwindow import ApplicationLogWindow, QTextEditLogger
|
|
from comictaggerlib.autotagmatchwindow import AutoTagMatchWindow
|
|
from comictaggerlib.autotagprogresswindow import AutoTagProgressWindow
|
|
from comictaggerlib.autotagstartwindow import AutoTagStartWindow
|
|
from comictaggerlib.cbltransformer import CBLTransformer
|
|
from comictaggerlib.coverimagewidget import CoverImageWidget
|
|
from comictaggerlib.crediteditorwindow import CreditEditorWindow
|
|
from comictaggerlib.exportwindow import ExportConflictOpts, ExportWindow
|
|
from comictaggerlib.fileselectionlist import FileInfo, FileSelectionList
|
|
from comictaggerlib.graphics import graphics_path
|
|
from comictaggerlib.issueidentifier import IssueIdentifier
|
|
from comictaggerlib.logwindow import LogWindow
|
|
from comictaggerlib.optionalmsgdialog import OptionalMessageDialog
|
|
from comictaggerlib.pagebrowser import PageBrowserWindow
|
|
from comictaggerlib.pagelisteditor import PageListEditor
|
|
from comictaggerlib.renamewindow import RenameWindow
|
|
from comictaggerlib.resulttypes import IssueResult, MultipleMatch, OnlineMatchResults
|
|
from comictaggerlib.seriesselectionwindow import SeriesSelectionWindow
|
|
from comictaggerlib.settingswindow import SettingsWindow
|
|
from comictaggerlib.ui import ui_path
|
|
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
|
|
from comictaggerlib.versionchecker import VersionChecker
|
|
from comictalker.comictalker import ComicTalker, TalkerError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def execute(f: Callable[[], Any]) -> None:
|
|
f()
|
|
|
|
|
|
class TaggerWindow(QtWidgets.QMainWindow):
|
|
appName = "ComicTagger"
|
|
version = ctversion.version
|
|
|
|
def __init__(
|
|
self,
|
|
file_list: list[str],
|
|
config: settngs.Config[settngs.Namespace],
|
|
talkers: dict[str, ComicTalker],
|
|
parent: QtWidgets.QWidget | None = None,
|
|
) -> None:
|
|
super().__init__(parent)
|
|
|
|
uic.loadUi(ui_path / "taggerwindow.ui", self)
|
|
self.config = config
|
|
self.talkers = talkers
|
|
self.log_window = self.setup_logger()
|
|
|
|
# prevent multiple instances
|
|
socket = QtNetwork.QLocalSocket(self)
|
|
socket.connectToServer(config[0].internal_install_id)
|
|
alive = socket.waitForConnected(3000)
|
|
if alive:
|
|
logger.setLevel(logging.INFO)
|
|
logger.info("Another application with key [%s] is already running", config[0].internal_install_id)
|
|
# send file list to other instance
|
|
if file_list:
|
|
socket.write(pickle.dumps(file_list))
|
|
if not socket.waitForBytesWritten(3000):
|
|
logger.error(socket.errorString())
|
|
socket.disconnectFromServer()
|
|
sys.exit()
|
|
else:
|
|
# listen on a socket to prevent multiple instances
|
|
self.socketServer = QtNetwork.QLocalServer(self)
|
|
self.socketServer.newConnection.connect(self.on_incoming_socket_connection)
|
|
ok = self.socketServer.listen(config[0].internal_install_id)
|
|
if not ok:
|
|
if self.socketServer.serverError() == QtNetwork.QAbstractSocket.SocketError.AddressInUseError:
|
|
self.socketServer.removeServer(config[0].internal_install_id)
|
|
ok = self.socketServer.listen(config[0].internal_install_id)
|
|
if not ok:
|
|
logger.error(
|
|
"Cannot start local socket with key [%s]. Reason: %s",
|
|
config[0].internal_install_id,
|
|
self.socketServer.errorString(),
|
|
)
|
|
sys.exit()
|
|
|
|
self.archiveCoverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.ArchiveMode, None, None)
|
|
grid_layout = QtWidgets.QGridLayout(self.coverImageContainer)
|
|
grid_layout.addWidget(self.archiveCoverWidget)
|
|
grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
self.page_list_editor = PageListEditor(self.tabPages)
|
|
grid_layout = QtWidgets.QGridLayout(self.tabPages)
|
|
grid_layout.addWidget(self.page_list_editor)
|
|
|
|
self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.config[0], self.dirty_flag_verification)
|
|
grid_layout = QtWidgets.QGridLayout(self.widgetListHolder)
|
|
grid_layout.addWidget(self.fileSelectionList)
|
|
|
|
self.fileSelectionList.selectionChanged.connect(self.file_list_selection_changed)
|
|
self.fileSelectionList.listCleared.connect(self.file_list_cleared)
|
|
self.fileSelectionList.set_sorting(
|
|
self.config[0].internal_sort_column, QtCore.Qt.SortOrder(self.config[0].internal_sort_direction)
|
|
)
|
|
|
|
# we can't specify relative font sizes in the UI designer, so
|
|
# walk through all the labels in the main form, and make them
|
|
# a smidge smaller TODO: there has to be a better way to do this
|
|
for child in self.scrollAreaWidgetContents.children():
|
|
if isinstance(child, QtWidgets.QLabel):
|
|
f = child.font()
|
|
if f.pointSize() > 10:
|
|
f.setPointSize(f.pointSize() - 2)
|
|
f.setItalic(True)
|
|
child.setFont(f)
|
|
|
|
self.scrollAreaWidgetContents.adjustSize()
|
|
|
|
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
|
|
|
if config[0].runtime_type and isinstance(config[0].runtime_type[0], int):
|
|
# respect the command line option tag type
|
|
config[0].internal_save_data_style = config[0].runtime_type[0]
|
|
config[0].internal_load_data_style = config[0].runtime_type[0]
|
|
|
|
self.save_data_style = config[0].internal_save_data_style
|
|
self.load_data_style = config[0].internal_load_data_style
|
|
|
|
self.setAcceptDrops(True)
|
|
self.config_menus()
|
|
self.statusBar()
|
|
self.populate_combo_boxes()
|
|
|
|
self.page_browser: PageBrowserWindow | None = None
|
|
self.comic_archive: ComicArchive | None = None
|
|
self.dirty_flag = False
|
|
self.droppedFile = None
|
|
self.page_loader = None
|
|
self.droppedFiles: list[str] = []
|
|
self.metadata = GenericMetadata()
|
|
self.atprogdialog: AutoTagProgressWindow | None = None
|
|
self.reset_app()
|
|
|
|
# set up some basic field validators
|
|
validator = QtGui.QIntValidator(1900, 2099, self)
|
|
self.lePubYear.setValidator(validator)
|
|
|
|
validator = QtGui.QIntValidator(1, 12, self)
|
|
self.lePubMonth.setValidator(validator)
|
|
|
|
# TODO: for now keep it simple, ideally we should check the full date
|
|
validator = QtGui.QIntValidator(1, 31, self)
|
|
self.lePubDay.setValidator(validator)
|
|
|
|
validator = QtGui.QIntValidator(1, 99999, self)
|
|
self.leIssueCount.setValidator(validator)
|
|
self.leVolumeNum.setValidator(validator)
|
|
self.leVolumeCount.setValidator(validator)
|
|
self.leAltIssueNum.setValidator(validator)
|
|
self.leAltIssueCount.setValidator(validator)
|
|
|
|
# TODO set up an RE validator for issueNum that allows for all sorts of wacky things
|
|
|
|
# tweak some control fonts
|
|
reduce_widget_font_size(self.lblFilename, 1)
|
|
reduce_widget_font_size(self.lblArchiveType)
|
|
reduce_widget_font_size(self.lblTagList)
|
|
reduce_widget_font_size(self.lblPageCount)
|
|
|
|
# make sure some editable comboboxes don't take drop actions
|
|
self.cbFormat.lineEdit().setAcceptDrops(False)
|
|
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
|
|
|
|
# hook up the callbacks
|
|
self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style)
|
|
self.cbSaveDataStyle.currentIndexChanged.connect(self.set_save_data_style)
|
|
self.btnEditCredit.clicked.connect(self.edit_credit)
|
|
self.btnAddCredit.clicked.connect(self.add_credit)
|
|
self.btnRemoveCredit.clicked.connect(self.remove_credit)
|
|
self.twCredits.cellDoubleClicked.connect(self.edit_credit)
|
|
self.btnOpenWebLink.clicked.connect(self.open_web_link)
|
|
self.connect_dirty_flag_signals()
|
|
self.page_list_editor.modified.connect(self.set_dirty_flag)
|
|
self.page_list_editor.firstFrontCoverChanged.connect(self.front_cover_changed)
|
|
self.page_list_editor.listOrderChanged.connect(self.page_list_order_changed)
|
|
self.tabWidget.currentChanged.connect(self.tab_changed)
|
|
|
|
self.update_style_tweaks()
|
|
|
|
self.show()
|
|
self.set_app_position()
|
|
if self.config[0].internal_form_width != -1:
|
|
self.splitter.setSizes([self.config[0].internal_form_width, self.config[0].internal_list_width])
|
|
self.raise_()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.resizeEvent(None)
|
|
|
|
self.splitter.splitterMoved.connect(self.splitter_moved_event)
|
|
|
|
self.fileSelectionList.add_app_action(self.actionAutoIdentify)
|
|
self.fileSelectionList.add_app_action(self.actionAutoTag)
|
|
self.fileSelectionList.add_app_action(self.actionCopyTags)
|
|
self.fileSelectionList.add_app_action(self.actionRename)
|
|
self.fileSelectionList.add_app_action(self.actionRemoveAuto)
|
|
self.fileSelectionList.add_app_action(self.actionRepackage)
|
|
|
|
if len(file_list) != 0:
|
|
self.fileSelectionList.add_path_list(file_list)
|
|
|
|
if self.config[0].dialog_show_disclaimer:
|
|
checked = OptionalMessageDialog.msg(
|
|
self,
|
|
"Welcome!",
|
|
"""
|
|
Thanks for trying ComicTagger!<br><br>
|
|
Be aware that this is beta-level software, and consider it experimental.
|
|
You should use it very carefully when modifying your data files. As the
|
|
license says, it's "AS IS!"<br><br>
|
|
Also, be aware that writing tags to comic archives will change their file hashes,
|
|
which has implications with respect to other software packages. It's best to
|
|
use ComicTagger on local copies of your comics.<br><br>
|
|
Have fun!
|
|
""",
|
|
)
|
|
self.config[0].dialog_show_disclaimer = not checked
|
|
|
|
if self.config[0].general_check_for_new_version:
|
|
self.check_latest_version_online()
|
|
|
|
def current_talker(self) -> ComicTalker:
|
|
if self.config[0].talker_source in self.talkers:
|
|
return self.talkers[self.config[0].talker_source]
|
|
logger.error("Could not find the '%s' talker", self.config[0].talker_source)
|
|
raise SystemExit(2)
|
|
|
|
def open_file_event(self, url: QtCore.QUrl) -> None:
|
|
logger.info(url.toLocalFile())
|
|
self.fileSelectionList.add_path_list([url.toLocalFile()])
|
|
|
|
def sigint_handler(self, *args: Any) -> None:
|
|
# defer the actual close in the app loop thread
|
|
QtCore.QTimer.singleShot(200, lambda: execute(self.close))
|
|
|
|
def setup_logger(self) -> ApplicationLogWindow:
|
|
try:
|
|
current_logs = (self.config[0].runtime_config.user_log_dir / "ComicTagger.log").read_text("utf-8")
|
|
except Exception:
|
|
current_logs = ""
|
|
root_logger = logging.getLogger()
|
|
qapplogwindow = ApplicationLogWindow(
|
|
QTextEditLogger(logging.Formatter("%(asctime)s | %(name)s | %(levelname)s | %(message)s"), logging.DEBUG),
|
|
parent=self,
|
|
)
|
|
qapplogwindow.textEdit.append(current_logs.strip())
|
|
root_logger.addHandler(qapplogwindow.log_handler)
|
|
return qapplogwindow
|
|
|
|
def reset_app(self) -> None:
|
|
self.archiveCoverWidget.clear()
|
|
self.comic_archive = None
|
|
self.dirty_flag = False
|
|
self.clear_form()
|
|
self.page_list_editor.reset_page()
|
|
if self.page_browser is not None:
|
|
self.page_browser.reset()
|
|
self.update_app_title()
|
|
self.update_menus()
|
|
self.update_info_box()
|
|
|
|
self.droppedFile = None
|
|
self.page_loader = None
|
|
|
|
def update_app_title(self) -> None:
|
|
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
|
|
|
if self.comic_archive is None:
|
|
self.setWindowTitle(self.appName)
|
|
else:
|
|
mod_str = ""
|
|
ro_str = ""
|
|
|
|
if self.dirty_flag:
|
|
mod_str = " [modified]"
|
|
|
|
if not self.comic_archive.is_writable():
|
|
ro_str = " [read only]"
|
|
|
|
self.setWindowTitle(f"{self.appName} - {self.comic_archive.path}{mod_str}{ro_str}")
|
|
|
|
def config_menus(self) -> None:
|
|
# File Menu
|
|
self.actionExit.setShortcut("Ctrl+Q")
|
|
self.actionExit.setStatusTip("Exit application")
|
|
self.actionExit.triggered.connect(self.close)
|
|
|
|
self.actionLoad.setShortcut("Ctrl+O")
|
|
self.actionLoad.setStatusTip("Load comic archive")
|
|
self.actionLoad.triggered.connect(self.select_file)
|
|
|
|
self.actionLoadFolder.setShortcut("Ctrl+Shift+O")
|
|
self.actionLoadFolder.setStatusTip("Load folder with comic archives")
|
|
self.actionLoadFolder.triggered.connect(self.select_folder)
|
|
|
|
self.actionWrite_Tags.setShortcut("Ctrl+S")
|
|
self.actionWrite_Tags.setStatusTip("Save tags to comic archive")
|
|
self.actionWrite_Tags.triggered.connect(self.commit_metadata)
|
|
|
|
self.actionAutoTag.setShortcut("Ctrl+T")
|
|
self.actionAutoTag.setStatusTip("Auto-tag multiple archives")
|
|
self.actionAutoTag.triggered.connect(self.auto_tag)
|
|
|
|
self.actionCopyTags.setShortcut("Ctrl+C")
|
|
self.actionCopyTags.setStatusTip("Copy one tag style to another")
|
|
self.actionCopyTags.triggered.connect(self.copy_tags)
|
|
|
|
self.actionRemoveAuto.setShortcut("Ctrl+D")
|
|
self.actionRemoveAuto.setStatusTip("Remove currently selected modify tag style from the archive")
|
|
self.actionRemoveAuto.triggered.connect(self.remove_auto)
|
|
|
|
self.actionRemoveCBLTags.setStatusTip("Remove ComicBookLover tags from comic archive")
|
|
self.actionRemoveCBLTags.triggered.connect(self.remove_cbl_tags)
|
|
|
|
self.actionRemoveCRTags.setStatusTip("Remove ComicRack tags from comic archive")
|
|
self.actionRemoveCRTags.triggered.connect(self.remove_cr_tags)
|
|
|
|
self.actionViewRawCRTags.setStatusTip("View raw ComicRack tag block from file")
|
|
self.actionViewRawCRTags.triggered.connect(self.view_raw_cr_tags)
|
|
|
|
self.actionViewRawCBLTags.setStatusTip("View raw ComicBookLover tag block from file")
|
|
self.actionViewRawCBLTags.triggered.connect(self.view_raw_cbl_tags)
|
|
|
|
self.actionRepackage.setShortcut("Ctrl+E")
|
|
self.actionRepackage.setStatusTip("Re-create archive as CBZ")
|
|
self.actionRepackage.triggered.connect(self.repackage_archive)
|
|
|
|
self.actionRename.setShortcut("Ctrl+N")
|
|
self.actionRename.setStatusTip("Rename archive based on tags")
|
|
self.actionRename.triggered.connect(self.rename_archive)
|
|
|
|
self.actionSettings.setShortcut("Ctrl+Shift+S")
|
|
self.actionSettings.setStatusTip("Configure ComicTagger")
|
|
self.actionSettings.triggered.connect(self.show_settings)
|
|
|
|
# Tag Menu
|
|
self.actionParse_Filename.setShortcut("Ctrl+F")
|
|
self.actionParse_Filename.setStatusTip("Try to extract tags from filename")
|
|
self.actionParse_Filename.triggered.connect(self.use_filename)
|
|
|
|
self.actionParse_Filename_split_words.setShortcut("Ctrl+Shift+F")
|
|
self.actionParse_Filename_split_words.setStatusTip("Try to extract tags from filename and split words")
|
|
self.actionParse_Filename_split_words.triggered.connect(self.use_filename_split)
|
|
|
|
self.actionSearchOnline.setShortcut("Ctrl+W")
|
|
self.actionSearchOnline.setStatusTip("Search online for tags")
|
|
self.actionSearchOnline.triggered.connect(self.query_online)
|
|
|
|
self.actionAutoImprint.triggered.connect(self.auto_imprint)
|
|
|
|
self.actionAutoIdentify.setShortcut("Ctrl+I")
|
|
self.actionAutoIdentify.triggered.connect(self.auto_identify_search)
|
|
|
|
self.actionLiteralSearch.triggered.connect(self.literal_search)
|
|
|
|
self.actionApplyCBLTransform.setShortcut("Ctrl+L")
|
|
self.actionApplyCBLTransform.setStatusTip("Modify tags specifically for CBL format")
|
|
self.actionApplyCBLTransform.triggered.connect(self.apply_cbl_transform)
|
|
|
|
self.actionReCalcPageDims.setShortcut("Ctrl+R")
|
|
self.actionReCalcPageDims.setStatusTip(
|
|
"Trigger re-calculating image size, height and width for all pages on the next save"
|
|
)
|
|
self.actionReCalcPageDims.triggered.connect(self.recalc_page_dimensions)
|
|
|
|
self.actionClearEntryForm.setShortcut("Ctrl+Shift+C")
|
|
self.actionClearEntryForm.setStatusTip("Clear all the data on the screen")
|
|
self.actionClearEntryForm.triggered.connect(self.clear_form)
|
|
|
|
# Window Menu
|
|
self.actionPageBrowser.setShortcut("Ctrl+P")
|
|
self.actionPageBrowser.setStatusTip("Show the page browser")
|
|
self.actionPageBrowser.triggered.connect(self.show_page_browser)
|
|
self.actionLogWindow.setShortcut("Ctrl+Shift+L")
|
|
self.actionLogWindow.setStatusTip("Show the log window")
|
|
self.actionLogWindow.triggered.connect(self.log_window.show)
|
|
|
|
# Help Menu
|
|
self.actionAbout.setStatusTip("Show the " + self.appName + " info")
|
|
self.actionAbout.triggered.connect(self.about_app)
|
|
self.actionWiki.triggered.connect(self.show_wiki)
|
|
self.actionReportBug.triggered.connect(self.report_bug)
|
|
self.actionComicTaggerForum.triggered.connect(self.show_forum)
|
|
|
|
# Notes Menu
|
|
self.btnOpenWebLink.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
|
|
|
|
# ToolBar
|
|
self.actionLoad.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
|
|
self.actionLoadFolder.setIcon(QtGui.QIcon(str(graphics_path / "longbox.png")))
|
|
self.actionWrite_Tags.setIcon(QtGui.QIcon(str(graphics_path / "save.png")))
|
|
self.actionParse_Filename.setIcon(QtGui.QIcon(str(graphics_path / "parse.png")))
|
|
self.actionParse_Filename_split_words.setIcon(QtGui.QIcon(str(graphics_path / "parse.png")))
|
|
self.actionSearchOnline.setIcon(QtGui.QIcon(str(graphics_path / "search.png")))
|
|
self.actionLiteralSearch.setIcon(QtGui.QIcon(str(graphics_path / "search.png")))
|
|
self.actionAutoIdentify.setIcon(QtGui.QIcon(str(graphics_path / "auto.png")))
|
|
self.actionAutoTag.setIcon(QtGui.QIcon(str(graphics_path / "autotag.png")))
|
|
self.actionAutoImprint.setIcon(QtGui.QIcon(str(graphics_path / "autotag.png")))
|
|
self.actionClearEntryForm.setIcon(QtGui.QIcon(str(graphics_path / "clear.png")))
|
|
self.actionPageBrowser.setIcon(QtGui.QIcon(str(graphics_path / "browse.png")))
|
|
|
|
self.toolBar.addAction(self.actionLoad)
|
|
self.toolBar.addAction(self.actionLoadFolder)
|
|
self.toolBar.addAction(self.actionWrite_Tags)
|
|
self.toolBar.addAction(self.actionSearchOnline)
|
|
self.toolBar.addAction(self.actionLiteralSearch)
|
|
self.toolBar.addAction(self.actionAutoIdentify)
|
|
self.toolBar.addAction(self.actionAutoTag)
|
|
self.toolBar.addAction(self.actionClearEntryForm)
|
|
self.toolBar.addAction(self.actionPageBrowser)
|
|
self.toolBar.addAction(self.actionAutoImprint)
|
|
|
|
def repackage_archive(self) -> None:
|
|
ca_list = self.fileSelectionList.get_selected_archive_list()
|
|
non_zip_count = 0
|
|
for ca in ca_list:
|
|
if not ca.is_zip():
|
|
non_zip_count += 1
|
|
|
|
if non_zip_count == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self, self.tr("Export as Zip Archive"), self.tr("Only ZIP archives are selected!")
|
|
)
|
|
logger.warning("Export as Zip Archive. Only ZIP archives are selected")
|
|
return
|
|
|
|
if not self.dirty_flag_verification(
|
|
"Export as Zip Archive",
|
|
"If you export archives as Zip now, unsaved data in the form may be lost. Are you sure?",
|
|
):
|
|
return
|
|
|
|
if non_zip_count != 0:
|
|
EW = ExportWindow(
|
|
self,
|
|
(
|
|
f"You have selected {non_zip_count} archive(s) to export to Zip format. "
|
|
""" New archives will be created in the same folder as the original.
|
|
|
|
Please choose config below, and select OK.
|
|
"""
|
|
),
|
|
)
|
|
EW.adjustSize()
|
|
EW.setModal(True)
|
|
if not EW.exec():
|
|
return
|
|
|
|
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, non_zip_count, self)
|
|
prog_dialog.setWindowTitle("Exporting as ZIP")
|
|
prog_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
|
prog_dialog.setMinimumDuration(300)
|
|
center_window_on_parent(prog_dialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
prog_idx = 0
|
|
|
|
new_archives_to_add = []
|
|
archives_to_remove = []
|
|
skipped_list = []
|
|
failed_list = []
|
|
success_count = 0
|
|
|
|
for ca in ca_list:
|
|
if not ca.is_zip():
|
|
QtCore.QCoreApplication.processEvents()
|
|
if prog_dialog.wasCanceled():
|
|
break
|
|
prog_idx += 1
|
|
prog_dialog.setValue(prog_idx)
|
|
prog_dialog.setLabelText(str(ca.path))
|
|
center_window_on_parent(prog_dialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
export_name = ca.path.with_suffix(".cbz")
|
|
export = True
|
|
|
|
if export_name.exists():
|
|
if EW.fileConflictBehavior == ExportConflictOpts.dontCreate:
|
|
export = False
|
|
skipped_list.append(ca.path)
|
|
elif EW.fileConflictBehavior == ExportConflictOpts.createUnique:
|
|
export_name = utils.unique_file(export_name)
|
|
|
|
if export:
|
|
if ca.export_as_zip(export_name):
|
|
success_count += 1
|
|
if EW.addToList:
|
|
new_archives_to_add.append(str(export_name))
|
|
if EW.deleteOriginal:
|
|
archives_to_remove.append(ca)
|
|
ca.path.unlink(missing_ok=True)
|
|
|
|
else:
|
|
# last export failed, so remove the zip, if it exists
|
|
failed_list.append(ca.path)
|
|
if export_name.exists():
|
|
export_name.unlink(missing_ok=True)
|
|
|
|
prog_dialog.hide()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.fileSelectionList.add_path_list(new_archives_to_add)
|
|
self.fileSelectionList.remove_archive_list(archives_to_remove)
|
|
|
|
summary = f"Successfully created {success_count} Zip archive(s)."
|
|
if len(skipped_list) > 0:
|
|
summary += (
|
|
f"\n\nThe following {len(skipped_list)} archive(s) were skipped due to file name conflicts:\n"
|
|
)
|
|
for f in skipped_list:
|
|
summary += f"\t{f}\n"
|
|
if len(failed_list) > 0:
|
|
summary += (
|
|
f"\n\nThe following {len(failed_list)} archive(s) failed to export due to read/write errors:\n"
|
|
)
|
|
for f in failed_list:
|
|
summary += f"\t{f}\n"
|
|
|
|
logger.info(summary)
|
|
dlg = LogWindow(self)
|
|
dlg.set_text(summary)
|
|
dlg.setWindowTitle("Archive Export to Zip Summary")
|
|
dlg.exec()
|
|
|
|
def about_app(self) -> None:
|
|
website = "https://github.com/comictagger/comictagger"
|
|
email = "comictagger@gmail.com"
|
|
license_link = "http://www.apache.org/licenses/LICENSE-2.0"
|
|
license_name = "Apache License 2.0"
|
|
|
|
msg_box = QtWidgets.QMessageBox()
|
|
msg_box.setWindowTitle("About " + self.appName)
|
|
msg_box.setTextFormat(QtCore.Qt.TextFormat.RichText)
|
|
msg_box.setIconPixmap(QtGui.QPixmap(str(graphics_path / "about.png")))
|
|
msg_box.setText(
|
|
"<br><br><br>"
|
|
+ self.appName
|
|
+ f" v{self.version}"
|
|
+ "<br>"
|
|
+ "©2014-2022 ComicTagger Devs<br><br>"
|
|
+ f"<a href='{website}'>{website}</a><br><br>"
|
|
+ f"<a href='mailto:{email}'>{email}</a><br><br>"
|
|
+ f"License: <a href='{license_link}'>{license_name}</a>"
|
|
)
|
|
|
|
msg_box.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
|
|
msg_box.exec()
|
|
|
|
def dragEnterEvent(self, event: QtGui.QDragEnterEvent) -> None:
|
|
self.droppedFiles = []
|
|
if event.mimeData().hasUrls():
|
|
# walk through the URL list and build a file list
|
|
for url in event.mimeData().urls():
|
|
if url.isValid() and url.scheme() == "file":
|
|
self.droppedFiles.append(url.toLocalFile())
|
|
|
|
if self.droppedFiles is not None:
|
|
event.accept()
|
|
|
|
def dropEvent(self, event: QtGui.QDropEvent) -> None:
|
|
self.fileSelectionList.add_path_list(self.droppedFiles)
|
|
event.accept()
|
|
|
|
def actual_load_current_archive(self) -> None:
|
|
if self.metadata.is_empty and self.comic_archive is not None:
|
|
self.metadata = self.comic_archive.metadata_from_filename(
|
|
self.config[0].filename_complicated_parser,
|
|
self.config[0].filename_remove_c2c,
|
|
self.config[0].filename_remove_fcbd,
|
|
self.config[0].filename_remove_publisher,
|
|
)
|
|
if len(self.metadata.pages) == 0 and self.comic_archive is not None:
|
|
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
|
|
|
|
self.update_cover_image()
|
|
|
|
if self.page_browser is not None and self.comic_archive is not None:
|
|
self.page_browser.set_comic_archive(self.comic_archive)
|
|
self.page_browser.metadata = self.metadata
|
|
|
|
if self.comic_archive is not None:
|
|
self.page_list_editor.set_data(self.comic_archive, self.metadata.pages)
|
|
self.metadata_to_form()
|
|
self.clear_dirty_flag() # also updates the app title
|
|
self.update_info_box()
|
|
self.update_menus()
|
|
self.update_app_title()
|
|
|
|
def update_cover_image(self) -> None:
|
|
cover_idx = self.metadata.get_cover_page_index_list()[0]
|
|
if self.comic_archive is not None:
|
|
self.archiveCoverWidget.set_archive(self.comic_archive, cover_idx)
|
|
|
|
def update_menus(self) -> None:
|
|
# First just disable all the questionable items
|
|
self.actionAutoTag.setEnabled(False)
|
|
self.actionCopyTags.setEnabled(False)
|
|
self.actionRemoveAuto.setEnabled(False)
|
|
self.actionRemoveCRTags.setEnabled(False)
|
|
self.actionRemoveCBLTags.setEnabled(False)
|
|
self.actionWrite_Tags.setEnabled(False)
|
|
self.actionRepackage.setEnabled(False)
|
|
self.actionViewRawCBLTags.setEnabled(False)
|
|
self.actionViewRawCRTags.setEnabled(False)
|
|
self.actionParse_Filename.setEnabled(False)
|
|
self.actionParse_Filename_split_words.setEnabled(False)
|
|
self.actionAutoIdentify.setEnabled(False)
|
|
self.actionRename.setEnabled(False)
|
|
self.actionApplyCBLTransform.setEnabled(False)
|
|
self.actionReCalcPageDims.setEnabled(False)
|
|
|
|
# now, selectively re-enable
|
|
if self.comic_archive is not None:
|
|
has_cix = self.comic_archive.has_cix()
|
|
has_cbi = self.comic_archive.has_cbi()
|
|
|
|
self.actionParse_Filename.setEnabled(True)
|
|
self.actionParse_Filename_split_words.setEnabled(True)
|
|
self.actionAutoIdentify.setEnabled(True)
|
|
self.actionAutoTag.setEnabled(True)
|
|
self.actionRename.setEnabled(True)
|
|
self.actionApplyCBLTransform.setEnabled(True)
|
|
self.actionReCalcPageDims.setEnabled(True)
|
|
self.actionRepackage.setEnabled(True)
|
|
self.actionRemoveAuto.setEnabled(True)
|
|
self.actionRemoveCRTags.setEnabled(True)
|
|
self.actionRemoveCBLTags.setEnabled(True)
|
|
self.actionCopyTags.setEnabled(True)
|
|
|
|
if has_cix:
|
|
self.actionViewRawCRTags.setEnabled(True)
|
|
if has_cbi:
|
|
self.actionViewRawCBLTags.setEnabled(True)
|
|
|
|
if self.comic_archive.is_writable():
|
|
self.actionWrite_Tags.setEnabled(True)
|
|
|
|
def update_info_box(self) -> None:
|
|
ca = self.comic_archive
|
|
|
|
if ca is None:
|
|
self.lblFilename.setText("")
|
|
self.lblArchiveType.setText("")
|
|
self.lblTagList.setText("")
|
|
self.lblPageCount.setText("")
|
|
return
|
|
|
|
filename = os.path.basename(ca.path)
|
|
filename = os.path.splitext(filename)[0]
|
|
filename = FileNameParser().fix_spaces(filename, False)
|
|
|
|
self.lblFilename.setText(filename)
|
|
|
|
self.lblArchiveType.setText(ca.archiver.name() + " archive")
|
|
|
|
page_count = f" ({ca.get_number_of_pages()} pages)"
|
|
self.lblPageCount.setText(page_count)
|
|
|
|
tag_info = ""
|
|
if ca.has_cix():
|
|
tag_info = "• ComicRack tags"
|
|
if ca.has_cbi():
|
|
if tag_info != "":
|
|
tag_info += "\n"
|
|
tag_info += "• ComicBookLover tags"
|
|
|
|
self.lblTagList.setText(tag_info)
|
|
|
|
def set_dirty_flag(self) -> None:
|
|
if not self.dirty_flag:
|
|
self.dirty_flag = True
|
|
self.fileSelectionList.set_modified_flag(True)
|
|
self.update_app_title()
|
|
|
|
def clear_dirty_flag(self) -> None:
|
|
if self.dirty_flag:
|
|
self.dirty_flag = False
|
|
self.fileSelectionList.set_modified_flag(False)
|
|
self.update_app_title()
|
|
|
|
def connect_dirty_flag_signals(self) -> None:
|
|
# recursively connect the tab form child slots
|
|
self.connect_child_dirty_flag_signals(self.tabWidget)
|
|
|
|
def connect_child_dirty_flag_signals(self, widget: QtCore.QObject) -> None:
|
|
if isinstance(widget, QtWidgets.QLineEdit):
|
|
widget.textEdited.connect(self.set_dirty_flag)
|
|
if isinstance(widget, QtWidgets.QTextEdit):
|
|
widget.textChanged.connect(self.set_dirty_flag)
|
|
if isinstance(widget, QtWidgets.QComboBox):
|
|
widget.currentIndexChanged.connect(self.set_dirty_flag)
|
|
if isinstance(widget, QtWidgets.QCheckBox):
|
|
widget.stateChanged.connect(self.set_dirty_flag)
|
|
|
|
# recursive call on children
|
|
for child in widget.children():
|
|
if child != self.page_list_editor:
|
|
self.connect_child_dirty_flag_signals(child)
|
|
|
|
def clear_form(self) -> None:
|
|
# get a minty fresh metadata object
|
|
self.metadata = GenericMetadata()
|
|
if self.comic_archive is not None:
|
|
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
|
|
self.page_list_editor.set_data(self.comic_archive, self.metadata.pages)
|
|
|
|
# recursively clear the tab form
|
|
self.clear_children(self.tabWidget)
|
|
|
|
# clear the dirty flag, since there is nothing in there now to lose
|
|
self.clear_dirty_flag()
|
|
|
|
def clear_children(self, widget: QtCore.QObject) -> None:
|
|
if isinstance(widget, (QtWidgets.QLineEdit, QtWidgets.QTextEdit)):
|
|
widget.setText("")
|
|
if isinstance(widget, QtWidgets.QComboBox):
|
|
widget.setCurrentIndex(0)
|
|
if isinstance(widget, QtWidgets.QCheckBox):
|
|
widget.setChecked(False)
|
|
if isinstance(widget, QtWidgets.QTableWidget):
|
|
widget.setRowCount(0)
|
|
|
|
# recursive call on children
|
|
for child in widget.children():
|
|
self.clear_children(child)
|
|
|
|
# Copy all of the metadata object into the form.
|
|
# Merging of metadata should be done via the overlay function
|
|
def metadata_to_form(self) -> None:
|
|
def assign_text(field: QtWidgets.QLineEdit | QtWidgets.QTextEdit, value: Any) -> None:
|
|
if value is not None:
|
|
field.setText(str(value))
|
|
|
|
md = self.metadata
|
|
|
|
assign_text(self.leSeries, md.series)
|
|
assign_text(self.leIssueNum, md.issue)
|
|
assign_text(self.leIssueCount, md.issue_count)
|
|
assign_text(self.leVolumeNum, md.volume)
|
|
assign_text(self.leVolumeCount, md.volume_count)
|
|
assign_text(self.leTitle, md.title)
|
|
assign_text(self.lePublisher, md.publisher)
|
|
assign_text(self.lePubMonth, md.month)
|
|
assign_text(self.lePubYear, md.year)
|
|
assign_text(self.lePubDay, md.day)
|
|
assign_text(self.leGenre, md.genre)
|
|
assign_text(self.leImprint, md.imprint)
|
|
assign_text(self.teComments, md.comments)
|
|
assign_text(self.teNotes, md.notes)
|
|
assign_text(self.leStoryArc, md.story_arc)
|
|
assign_text(self.leScanInfo, md.scan_info)
|
|
assign_text(self.leSeriesGroup, md.series_group)
|
|
assign_text(self.leAltSeries, md.alternate_series)
|
|
assign_text(self.leAltIssueNum, md.alternate_number)
|
|
assign_text(self.leAltIssueCount, md.alternate_count)
|
|
assign_text(self.leWebLink, md.web_link)
|
|
assign_text(self.teCharacters, md.characters)
|
|
assign_text(self.teTeams, md.teams)
|
|
assign_text(self.teLocations, md.locations)
|
|
|
|
self.dsbCriticalRating.setValue(md.critical_rating or 0.0)
|
|
|
|
if md.format is not None and md.format != "":
|
|
i = self.cbFormat.findText(md.format)
|
|
if i == -1:
|
|
self.cbFormat.setEditText(md.format)
|
|
else:
|
|
self.cbFormat.setCurrentIndex(i)
|
|
|
|
if md.maturity_rating is not None and md.maturity_rating != "":
|
|
i = self.cbMaturityRating.findText(md.maturity_rating)
|
|
if i == -1:
|
|
self.cbMaturityRating.setEditText(md.maturity_rating)
|
|
else:
|
|
self.cbMaturityRating.setCurrentIndex(i)
|
|
else:
|
|
self.cbMaturityRating.setCurrentIndex(0)
|
|
|
|
if md.language is not None:
|
|
i = self.cbLanguage.findData(md.language)
|
|
self.cbLanguage.setCurrentIndex(i)
|
|
else:
|
|
self.cbLanguage.setCurrentIndex(0)
|
|
|
|
if md.country is not None:
|
|
i = self.cbCountry.findText(md.country)
|
|
self.cbCountry.setCurrentIndex(i)
|
|
else:
|
|
self.cbCountry.setCurrentIndex(0)
|
|
|
|
if md.manga is not None:
|
|
i = self.cbManga.findData(md.manga)
|
|
self.cbManga.setCurrentIndex(i)
|
|
else:
|
|
self.cbManga.setCurrentIndex(0)
|
|
|
|
if md.black_and_white:
|
|
self.cbBW.setChecked(True)
|
|
else:
|
|
self.cbBW.setChecked(False)
|
|
|
|
self.teTags.setText(", ".join(md.tags))
|
|
|
|
self.twCredits.setRowCount(0)
|
|
|
|
if md.credits is not None and len(md.credits) != 0:
|
|
self.twCredits.setSortingEnabled(False)
|
|
|
|
row = 0
|
|
for credit in md.credits:
|
|
# if the role-person pair already exists, just skip adding it to the list
|
|
if self.is_dupe_credit(credit["role"].title(), credit["person"]):
|
|
continue
|
|
|
|
self.add_new_credit_entry(
|
|
row, credit["role"].title(), credit["person"], (credit["primary"] if "primary" in credit else False)
|
|
)
|
|
|
|
row += 1
|
|
|
|
self.twCredits.setSortingEnabled(True)
|
|
self.update_credit_colors()
|
|
|
|
def add_new_credit_entry(self, row: int, role: str, name: str, primary_flag: bool = False) -> None:
|
|
self.twCredits.insertRow(row)
|
|
|
|
item_text = role
|
|
item = QtWidgets.QTableWidgetItem(item_text)
|
|
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
|
self.twCredits.setItem(row, 1, item)
|
|
|
|
item_text = name
|
|
item = QtWidgets.QTableWidgetItem(item_text)
|
|
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
|
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.twCredits.setItem(row, 2, item)
|
|
|
|
item = QtWidgets.QTableWidgetItem("")
|
|
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
|
self.twCredits.setItem(row, 0, item)
|
|
self.update_credit_primary_flag(row, primary_flag)
|
|
|
|
def is_dupe_credit(self, role: str, name: str) -> bool:
|
|
for r in range(self.twCredits.rowCount()):
|
|
if self.twCredits.item(r, 1).text() == role and self.twCredits.item(r, 2).text() == name:
|
|
return True
|
|
|
|
return False
|
|
|
|
def form_to_metadata(self) -> None:
|
|
# copy the data from the form into the metadata
|
|
md = GenericMetadata()
|
|
md.is_empty = False
|
|
md.alternate_number = IssueString(self.leAltIssueNum.text()).as_string()
|
|
md.issue = IssueString(self.leIssueNum.text()).as_string()
|
|
md.issue_count = utils.xlate(self.leIssueCount.text(), True)
|
|
md.volume = utils.xlate(self.leVolumeNum.text(), True)
|
|
md.volume_count = utils.xlate(self.leVolumeCount.text(), True)
|
|
md.month = utils.xlate(self.lePubMonth.text(), True)
|
|
md.year = utils.xlate(self.lePubYear.text(), True)
|
|
md.day = utils.xlate(self.lePubDay.text(), True)
|
|
md.alternate_count = utils.xlate(self.leAltIssueCount.text(), True)
|
|
|
|
md.series = self.leSeries.text()
|
|
md.title = self.leTitle.text()
|
|
md.publisher = self.lePublisher.text()
|
|
md.genre = self.leGenre.text()
|
|
md.imprint = self.leImprint.text()
|
|
md.comments = self.teComments.toPlainText()
|
|
md.notes = self.teNotes.toPlainText()
|
|
md.maturity_rating = self.cbMaturityRating.currentText()
|
|
|
|
md.critical_rating = utils.xlate(self.dsbCriticalRating.cleanText(), is_float=True)
|
|
if md.critical_rating == 0.0:
|
|
md.critical_rating = None
|
|
|
|
md.story_arc = self.leStoryArc.text()
|
|
md.scan_info = self.leScanInfo.text()
|
|
md.series_group = self.leSeriesGroup.text()
|
|
md.alternate_series = self.leAltSeries.text()
|
|
md.web_link = self.leWebLink.text()
|
|
md.characters = self.teCharacters.toPlainText()
|
|
md.teams = self.teTeams.toPlainText()
|
|
md.locations = self.teLocations.toPlainText()
|
|
|
|
md.format = self.cbFormat.currentText()
|
|
md.country = self.cbCountry.currentText()
|
|
|
|
md.language = utils.xlate(self.cbLanguage.itemData(self.cbLanguage.currentIndex()))
|
|
|
|
md.manga = utils.xlate(self.cbManga.itemData(self.cbManga.currentIndex()))
|
|
|
|
# Make a list from the comma delimited tags string
|
|
tmp = self.teTags.toPlainText()
|
|
if tmp is not None:
|
|
|
|
def strip_list(i: Iterable[str]) -> set[str]:
|
|
return {x.strip() for x in i}
|
|
|
|
md.tags = strip_list(tmp.split(","))
|
|
|
|
md.black_and_white = self.cbBW.isChecked()
|
|
|
|
# get the credits from the table
|
|
md.credits = []
|
|
for row in range(self.twCredits.rowCount()):
|
|
role = self.twCredits.item(row, 1).text()
|
|
name = self.twCredits.item(row, 2).text()
|
|
primary_flag = self.twCredits.item(row, 0).text() != ""
|
|
|
|
md.add_credit(name, role, bool(primary_flag))
|
|
|
|
md.pages = self.page_list_editor.get_page_list()
|
|
self.metadata = md
|
|
|
|
def use_filename(self) -> None:
|
|
self._use_filename()
|
|
|
|
def _use_filename(self, split_words: bool = False) -> None:
|
|
if self.comic_archive is not None:
|
|
# copy the form onto metadata object
|
|
self.form_to_metadata()
|
|
new_metadata = self.comic_archive.metadata_from_filename(
|
|
self.config[0].filename_complicated_parser,
|
|
self.config[0].filename_remove_c2c,
|
|
self.config[0].filename_remove_fcbd,
|
|
self.config[0].filename_remove_publisher,
|
|
split_words,
|
|
)
|
|
if new_metadata is not None:
|
|
self.metadata.overlay(new_metadata)
|
|
self.metadata_to_form()
|
|
|
|
def use_filename_split(self) -> None:
|
|
self._use_filename(True)
|
|
|
|
def select_folder(self) -> None:
|
|
self.select_file(folder_mode=True)
|
|
|
|
def select_file(self, folder_mode: bool = False) -> None:
|
|
dialog = QtWidgets.QFileDialog(self)
|
|
if folder_mode:
|
|
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.Directory)
|
|
else:
|
|
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles)
|
|
|
|
if self.config[0].internal_last_opened_folder is not None:
|
|
dialog.setDirectory(self.config[0].internal_last_opened_folder)
|
|
|
|
if not folder_mode:
|
|
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)"
|
|
filters = [archive_filter, "Any files (*)"]
|
|
dialog.setNameFilters(filters)
|
|
|
|
if dialog.exec():
|
|
file_list = dialog.selectedFiles()
|
|
self.fileSelectionList.add_path_list(file_list)
|
|
|
|
def auto_identify_search(self) -> None:
|
|
if self.comic_archive is None:
|
|
QtWidgets.QMessageBox.warning(self, "Automatic Identify Search", "You need to load a comic first!")
|
|
return
|
|
|
|
self.query_online(autoselect=True)
|
|
|
|
def literal_search(self) -> None:
|
|
self.query_online(autoselect=False, literal=True)
|
|
|
|
def query_online(self, autoselect: bool = False, literal: bool = False) -> None:
|
|
issue_number = str(self.leIssueNum.text()).strip()
|
|
|
|
# Only need this check is the source has issue level data.
|
|
if autoselect and issue_number == "":
|
|
QtWidgets.QMessageBox.information(
|
|
self, "Automatic Identify Search", "Can't auto-identify without an issue number (yet!)"
|
|
)
|
|
return
|
|
|
|
if str(self.leSeries.text()).strip() != "":
|
|
series_name = str(self.leSeries.text()).strip()
|
|
else:
|
|
QtWidgets.QMessageBox.information(self, "Online Search", "Need to enter a series name to search.")
|
|
return
|
|
|
|
year = utils.xlate(self.lePubYear.text(), True)
|
|
|
|
issue_count = utils.xlate(self.leIssueCount.text(), True)
|
|
|
|
cover_index_list = self.metadata.get_cover_page_index_list()
|
|
selector = SeriesSelectionWindow(
|
|
self,
|
|
series_name,
|
|
issue_number,
|
|
year,
|
|
issue_count,
|
|
cover_index_list,
|
|
self.comic_archive,
|
|
self.config[0],
|
|
self.current_talker(),
|
|
autoselect,
|
|
literal,
|
|
)
|
|
|
|
selector.setWindowTitle(f"Search: '{series_name}' - Select Series")
|
|
|
|
selector.setModal(True)
|
|
selector.exec()
|
|
|
|
if selector.result():
|
|
# we should now have a series ID
|
|
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
|
|
|
# copy the form onto metadata object
|
|
self.form_to_metadata()
|
|
|
|
try:
|
|
new_metadata = self.current_talker().fetch_comic_data(
|
|
issue_id=selector.issue_id, series_id=selector.series_id, issue_number=selector.issue_number
|
|
)
|
|
except TalkerError as e:
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
QtWidgets.QMessageBox.critical(self, f"{e.source} {e.code_name} Error", f"{e}")
|
|
else:
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
if new_metadata is not None:
|
|
if self.config[0].cbl_apply_transform_on_import:
|
|
new_metadata = CBLTransformer(new_metadata, self.config[0]).apply()
|
|
|
|
if self.config[0].identifier_clear_form_before_populating:
|
|
self.clear_form()
|
|
|
|
notes = (
|
|
f"Tagged with ComicTagger {ctversion.version} using info from {self.current_talker().name} on"
|
|
f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {new_metadata.issue_id}]"
|
|
)
|
|
self.metadata.overlay(
|
|
new_metadata.replace(
|
|
notes=utils.combine_notes(self.metadata.notes, notes, "Tagged with ComicTagger")
|
|
)
|
|
)
|
|
# Now push the new combined data into the edit controls
|
|
self.metadata_to_form()
|
|
else:
|
|
QtWidgets.QMessageBox.critical(
|
|
self, "Search", f"Could not find an issue {selector.issue_number} for that series"
|
|
)
|
|
|
|
def commit_metadata(self) -> None:
|
|
if self.metadata is not None and self.comic_archive is not None:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
"Save Tags",
|
|
f"Are you sure you wish to save {MetaDataStyle.name[self.save_data_style]} tags to this archive?",
|
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
|
QtWidgets.QMessageBox.StandardButton.No,
|
|
)
|
|
|
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
|
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
|
self.form_to_metadata()
|
|
|
|
success = self.comic_archive.write_metadata(self.metadata, self.save_data_style)
|
|
self.comic_archive.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
|
|
if not success:
|
|
QtWidgets.QMessageBox.warning(self, "Save failed", "The tag save operation seemed to fail!")
|
|
else:
|
|
self.clear_dirty_flag()
|
|
self.update_info_box()
|
|
self.update_menus()
|
|
self.fileSelectionList.update_current_row()
|
|
|
|
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
|
|
self.actual_load_current_archive()
|
|
else:
|
|
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!")
|
|
|
|
def set_load_data_style(self, s: int) -> None:
|
|
if self.dirty_flag_verification(
|
|
"Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?"
|
|
):
|
|
self.load_data_style = self.cbLoadDataStyle.itemData(s)
|
|
self.config[0].internal_load_data_style = self.load_data_style
|
|
self.update_menus()
|
|
if self.comic_archive is not None:
|
|
self.load_archive(self.comic_archive)
|
|
else:
|
|
self.cbLoadDataStyle.currentIndexChanged.disconnect(self.set_load_data_style)
|
|
self.adjust_load_style_combo()
|
|
self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style)
|
|
|
|
def set_save_data_style(self, s: int) -> None:
|
|
self.save_data_style = self.cbSaveDataStyle.itemData(s)
|
|
self.config[0].internal_save_data_style = self.save_data_style
|
|
self.update_style_tweaks()
|
|
self.update_menus()
|
|
|
|
def update_credit_colors(self) -> None:
|
|
# !!!ATB qt5 porting TODO
|
|
inactive_color = QtGui.QColor(255, 170, 150)
|
|
active_palette = self.leSeries.palette()
|
|
active_color = active_palette.color(QtGui.QPalette.ColorRole.Base)
|
|
|
|
inactive_brush = QtGui.QBrush(inactive_color)
|
|
active_brush = QtGui.QBrush(active_color)
|
|
|
|
cix_credits = ComicInfoXml().get_parseable_credits()
|
|
|
|
if self.save_data_style == MetaDataStyle.CIX:
|
|
# loop over credit table, mark selected rows
|
|
for r in range(self.twCredits.rowCount()):
|
|
if str(self.twCredits.item(r, 1).text()).casefold() not in cix_credits:
|
|
self.twCredits.item(r, 1).setBackground(inactive_brush)
|
|
else:
|
|
self.twCredits.item(r, 1).setBackground(active_brush)
|
|
# turn off entire primary column
|
|
self.twCredits.item(r, 0).setBackground(inactive_brush)
|
|
|
|
if self.save_data_style == MetaDataStyle.CBI:
|
|
# loop over credit table, make all active color
|
|
for r in range(self.twCredits.rowCount()):
|
|
self.twCredits.item(r, 0).setBackground(active_brush)
|
|
self.twCredits.item(r, 1).setBackground(active_brush)
|
|
|
|
def update_style_tweaks(self) -> None:
|
|
# depending on the current data style, certain fields are disabled
|
|
|
|
inactive_color = QtGui.QColor(255, 170, 150)
|
|
active_palette = self.leSeries.palette()
|
|
|
|
inactive_palette1 = self.leSeries.palette()
|
|
inactive_palette1.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
|
|
|
inactive_palette2 = self.leSeries.palette()
|
|
|
|
inactive_palette3 = self.leSeries.palette()
|
|
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
|
|
|
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
|
|
|
# helper func
|
|
def enable_widget(widget: QtWidgets.QWidget, enable: bool) -> None:
|
|
inactive_palette3.setColor(widget.backgroundRole(), inactive_color)
|
|
inactive_palette2.setColor(widget.backgroundRole(), inactive_color)
|
|
inactive_palette3.setColor(widget.foregroundRole(), inactive_color)
|
|
|
|
if enable:
|
|
widget.setPalette(active_palette)
|
|
widget.setAutoFillBackground(False)
|
|
if isinstance(widget, QtWidgets.QCheckBox):
|
|
widget.setEnabled(True)
|
|
elif isinstance(widget, QtWidgets.QComboBox):
|
|
widget.setEnabled(True)
|
|
elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
|
|
widget.setReadOnly(False)
|
|
else:
|
|
widget.setAutoFillBackground(True)
|
|
if isinstance(widget, QtWidgets.QCheckBox):
|
|
widget.setPalette(inactive_palette2)
|
|
widget.setEnabled(False)
|
|
elif isinstance(widget, QtWidgets.QComboBox):
|
|
widget.setPalette(inactive_palette3)
|
|
widget.setEnabled(False)
|
|
elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
|
|
widget.setReadOnly(True)
|
|
widget.setPalette(inactive_palette1)
|
|
|
|
cbi_only = [self.leVolumeCount, self.cbCountry, self.teTags]
|
|
cix_only = [
|
|
self.leImprint,
|
|
self.teNotes,
|
|
self.cbBW,
|
|
self.cbManga,
|
|
self.leStoryArc,
|
|
self.leScanInfo,
|
|
self.leSeriesGroup,
|
|
self.leAltSeries,
|
|
self.leAltIssueNum,
|
|
self.leAltIssueCount,
|
|
self.leWebLink,
|
|
self.teCharacters,
|
|
self.teTeams,
|
|
self.teLocations,
|
|
self.cbMaturityRating,
|
|
self.cbFormat,
|
|
]
|
|
|
|
if self.save_data_style == MetaDataStyle.CIX:
|
|
for item in cix_only:
|
|
enable_widget(item, True)
|
|
for item in cbi_only:
|
|
enable_widget(item, False)
|
|
|
|
if self.save_data_style == MetaDataStyle.CBI:
|
|
for item in cbi_only:
|
|
enable_widget(item, True)
|
|
for item in cix_only:
|
|
enable_widget(item, False)
|
|
|
|
self.update_credit_colors()
|
|
self.page_list_editor.set_metadata_style(self.save_data_style)
|
|
|
|
def cell_double_clicked(self, r: int, c: int) -> None:
|
|
self.edit_credit()
|
|
|
|
def add_credit(self) -> None:
|
|
self.modify_credits(False)
|
|
|
|
def edit_credit(self) -> None:
|
|
if self.twCredits.currentRow() > -1:
|
|
self.modify_credits(True)
|
|
|
|
def update_credit_primary_flag(self, row: int, primary: bool) -> None:
|
|
# if we're clearing a flag do it and quit
|
|
if not primary:
|
|
self.twCredits.item(row, 0).setText("")
|
|
return
|
|
|
|
# otherwise, we need to check for, and clear, other primaries with same role
|
|
role = str(self.twCredits.item(row, 1).text())
|
|
r = 0
|
|
for r in range(self.twCredits.rowCount()):
|
|
if (
|
|
self.twCredits.item(r, 0).text() != ""
|
|
and str(self.twCredits.item(r, 1).text()).casefold() == role.casefold()
|
|
):
|
|
self.twCredits.item(r, 0).setText("")
|
|
|
|
# Now set our new primary
|
|
self.twCredits.item(row, 0).setText("Yes")
|
|
|
|
def modify_credits(self, edit: bool) -> None:
|
|
if edit:
|
|
row = self.twCredits.currentRow()
|
|
role = self.twCredits.item(row, 1).text()
|
|
name = self.twCredits.item(row, 2).text()
|
|
primary = self.twCredits.item(row, 0).text() != ""
|
|
else:
|
|
row = self.twCredits.rowCount()
|
|
role = ""
|
|
name = ""
|
|
primary = False
|
|
|
|
editor = CreditEditorWindow(self, CreditEditorWindow.ModeEdit, role, name, primary)
|
|
editor.setModal(True)
|
|
editor.exec()
|
|
if editor.result():
|
|
new_role, new_name, new_primary = editor.get_credits()
|
|
|
|
if new_name == name and new_role == role and new_primary == primary:
|
|
# nothing has changed, just quit
|
|
return
|
|
|
|
# name and role is the same, but primary flag changed
|
|
if new_name == name and new_role == role:
|
|
self.update_credit_primary_flag(row, new_primary)
|
|
return
|
|
|
|
# check for dupes
|
|
ok_to_mod = True
|
|
if self.is_dupe_credit(new_role, new_name):
|
|
# delete the dupe credit from list
|
|
qmsg = QtWidgets.QMessageBox()
|
|
qmsg.setText("Duplicate Credit!")
|
|
qmsg.setInformativeText(
|
|
"This will create a duplicate credit entry. Would you like to merge the entries, or create a duplicate?"
|
|
)
|
|
qmsg.addButton("Merge", QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
|
qmsg.addButton("Duplicate", QtWidgets.QMessageBox.ButtonRole.NoRole)
|
|
|
|
if qmsg.exec() == 0:
|
|
# merge
|
|
if edit:
|
|
# just remove the row that would be same
|
|
self.twCredits.removeRow(row)
|
|
# TODO -- need to find the row of the dupe, and possible change the primary flag
|
|
|
|
ok_to_mod = False
|
|
|
|
if ok_to_mod:
|
|
# modify it
|
|
if edit:
|
|
self.twCredits.item(row, 1).setText(new_role)
|
|
self.twCredits.item(row, 2).setText(new_name)
|
|
self.update_credit_primary_flag(row, new_primary)
|
|
else:
|
|
# add new entry
|
|
row = self.twCredits.rowCount()
|
|
self.add_new_credit_entry(row, new_role, new_name, new_primary)
|
|
|
|
self.update_credit_colors()
|
|
self.set_dirty_flag()
|
|
|
|
def remove_credit(self) -> None:
|
|
row = self.twCredits.currentRow()
|
|
if row != -1:
|
|
self.twCredits.removeRow(row)
|
|
self.set_dirty_flag()
|
|
|
|
def open_web_link(self) -> None:
|
|
if self.leWebLink is not None:
|
|
web_link = self.leWebLink.text().strip()
|
|
try:
|
|
result = urlparse(web_link)
|
|
all([result.scheme in ["http", "https"], result.netloc])
|
|
webbrowser.open_new_tab(web_link)
|
|
except ValueError:
|
|
QtWidgets.QMessageBox.warning(self, self.tr("Web Link"), self.tr("Web Link is invalid."))
|
|
|
|
def show_settings(self) -> None:
|
|
settingswin = SettingsWindow(self, self.config, self.talkers)
|
|
settingswin.setModal(True)
|
|
settingswin.exec()
|
|
settingswin.result()
|
|
|
|
def set_app_position(self) -> None:
|
|
if self.config[0].internal_window_width != 0:
|
|
self.move(self.config[0].internal_window_x, self.config[0].internal_window_y)
|
|
self.resize(self.config[0].internal_window_width, self.config[0].internal_window_height)
|
|
else:
|
|
screen = QtGui.QGuiApplication.primaryScreen().geometry()
|
|
size = self.frameGeometry()
|
|
self.move(int((screen.width() - size.width()) / 2), int((screen.height() - size.height()) / 2))
|
|
|
|
def adjust_load_style_combo(self) -> None:
|
|
# select the current style
|
|
if self.load_data_style == MetaDataStyle.CBI:
|
|
self.cbLoadDataStyle.setCurrentIndex(0)
|
|
elif self.load_data_style == MetaDataStyle.CIX:
|
|
self.cbLoadDataStyle.setCurrentIndex(1)
|
|
|
|
def adjust_save_style_combo(self) -> None:
|
|
# select the current style
|
|
if self.save_data_style == MetaDataStyle.CBI:
|
|
self.cbSaveDataStyle.setCurrentIndex(0)
|
|
elif self.save_data_style == MetaDataStyle.CIX:
|
|
self.cbSaveDataStyle.setCurrentIndex(1)
|
|
self.update_style_tweaks()
|
|
|
|
def populate_combo_boxes(self) -> None:
|
|
# Add the entries to the tag style combobox
|
|
self.cbLoadDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI)
|
|
self.cbLoadDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
|
self.adjust_load_style_combo()
|
|
|
|
self.cbSaveDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI)
|
|
self.cbSaveDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
|
self.adjust_save_style_combo()
|
|
|
|
# Add the entries to the country combobox
|
|
self.cbCountry.addItem("", "")
|
|
for f in natsort.humansorted(utils.countries.items(), operator.itemgetter(1)):
|
|
self.cbCountry.addItem(f[1], f[0])
|
|
|
|
# Add the entries to the language combobox
|
|
self.cbLanguage.addItem("", "")
|
|
|
|
for f in natsort.humansorted(utils.languages.items(), operator.itemgetter(1)):
|
|
self.cbLanguage.addItem(f[1], f[0])
|
|
|
|
# Add the entries to the manga combobox
|
|
self.cbManga.addItem("", "")
|
|
self.cbManga.addItem("Yes", "Yes")
|
|
self.cbManga.addItem("Yes (Right to Left)", "YesAndRightToLeft")
|
|
self.cbManga.addItem("No", "No")
|
|
|
|
# Add the entries to the maturity combobox
|
|
self.cbMaturityRating.addItem("", "")
|
|
self.cbMaturityRating.addItem("Everyone", "")
|
|
self.cbMaturityRating.addItem("G", "")
|
|
self.cbMaturityRating.addItem("Early Childhood", "")
|
|
self.cbMaturityRating.addItem("Everyone 10+", "")
|
|
self.cbMaturityRating.addItem("PG", "")
|
|
self.cbMaturityRating.addItem("Kids to Adults", "")
|
|
self.cbMaturityRating.addItem("Teen", "")
|
|
self.cbMaturityRating.addItem("M", "")
|
|
self.cbMaturityRating.addItem("MA15+", "")
|
|
self.cbMaturityRating.addItem("Mature 17+", "")
|
|
self.cbMaturityRating.addItem("R18+", "")
|
|
self.cbMaturityRating.addItem("X18+", "")
|
|
self.cbMaturityRating.addItem("Adults Only 18+", "")
|
|
self.cbMaturityRating.addItem("Rating Pending", "")
|
|
|
|
# Add entries to the format combobox
|
|
self.cbFormat.addItem("")
|
|
self.cbFormat.addItem(".1")
|
|
self.cbFormat.addItem("-1")
|
|
self.cbFormat.addItem("1 Shot")
|
|
self.cbFormat.addItem("1/2")
|
|
self.cbFormat.addItem("1-Shot")
|
|
self.cbFormat.addItem("Annotation")
|
|
self.cbFormat.addItem("Annotations")
|
|
self.cbFormat.addItem("Annual")
|
|
self.cbFormat.addItem("Anthology")
|
|
self.cbFormat.addItem("B&W")
|
|
self.cbFormat.addItem("B/W")
|
|
self.cbFormat.addItem("B&&W")
|
|
self.cbFormat.addItem("Black & White")
|
|
self.cbFormat.addItem("Box Set")
|
|
self.cbFormat.addItem("Box-Set")
|
|
self.cbFormat.addItem("Crossover")
|
|
self.cbFormat.addItem("Director's Cut")
|
|
self.cbFormat.addItem("Epilogue")
|
|
self.cbFormat.addItem("Event")
|
|
self.cbFormat.addItem("FCBD")
|
|
self.cbFormat.addItem("Flyer")
|
|
self.cbFormat.addItem("Giant")
|
|
self.cbFormat.addItem("Giant Size")
|
|
self.cbFormat.addItem("Giant-Size")
|
|
self.cbFormat.addItem("Graphic Novel")
|
|
self.cbFormat.addItem("Hardcover")
|
|
self.cbFormat.addItem("Hard-Cover")
|
|
self.cbFormat.addItem("King")
|
|
self.cbFormat.addItem("King Size")
|
|
self.cbFormat.addItem("King-Size")
|
|
self.cbFormat.addItem("Limited Series")
|
|
self.cbFormat.addItem("Magazine")
|
|
self.cbFormat.addItem("-1")
|
|
self.cbFormat.addItem("NSFW")
|
|
self.cbFormat.addItem("One Shot")
|
|
self.cbFormat.addItem("One-Shot")
|
|
self.cbFormat.addItem("Point 1")
|
|
self.cbFormat.addItem("Preview")
|
|
self.cbFormat.addItem("Prologue")
|
|
self.cbFormat.addItem("Reference")
|
|
self.cbFormat.addItem("Review")
|
|
self.cbFormat.addItem("Reviewed")
|
|
self.cbFormat.addItem("Scanlation")
|
|
self.cbFormat.addItem("Script")
|
|
self.cbFormat.addItem("Series")
|
|
self.cbFormat.addItem("Sketch")
|
|
self.cbFormat.addItem("Special")
|
|
self.cbFormat.addItem("TPB")
|
|
self.cbFormat.addItem("Trade Paper Back")
|
|
self.cbFormat.addItem("WebComic")
|
|
self.cbFormat.addItem("Web Comic")
|
|
self.cbFormat.addItem("Year 1")
|
|
self.cbFormat.addItem("Year One")
|
|
|
|
def remove_auto(self) -> None:
|
|
self.remove_tags(self.save_data_style)
|
|
|
|
def remove_cbl_tags(self) -> None:
|
|
self.remove_tags(MetaDataStyle.CBI)
|
|
|
|
def remove_cr_tags(self) -> None:
|
|
self.remove_tags(MetaDataStyle.CIX)
|
|
|
|
def remove_tags(self, style: int) -> None:
|
|
# remove the indicated tags from the archive
|
|
ca_list = self.fileSelectionList.get_selected_archive_list()
|
|
has_md_count = 0
|
|
for ca in ca_list:
|
|
if ca.has_metadata(style):
|
|
has_md_count += 1
|
|
|
|
if has_md_count == 0:
|
|
QtWidgets.QMessageBox.information(
|
|
self, "Remove Tags", f"No archives with {MetaDataStyle.name[style]} tags selected!"
|
|
)
|
|
return
|
|
|
|
if has_md_count != 0 and not self.dirty_flag_verification(
|
|
"Remove Tags", "If you remove tags now, unsaved data in the form will be lost. Are you sure?"
|
|
):
|
|
return
|
|
|
|
if has_md_count != 0:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
"Remove Tags",
|
|
f"Are you sure you wish to remove the {MetaDataStyle.name[style]} tags from {has_md_count} archive(s)?",
|
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
|
QtWidgets.QMessageBox.StandardButton.No,
|
|
)
|
|
|
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
|
progdialog = QtWidgets.QProgressDialog("", "Cancel", 0, has_md_count, self)
|
|
progdialog.setWindowTitle("Removing Tags")
|
|
progdialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
|
progdialog.setMinimumDuration(300)
|
|
center_window_on_parent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
prog_idx = 0
|
|
|
|
failed_list = []
|
|
success_count = 0
|
|
for ca in ca_list:
|
|
if ca.has_metadata(style):
|
|
QtCore.QCoreApplication.processEvents()
|
|
if progdialog.wasCanceled():
|
|
break
|
|
prog_idx += 1
|
|
progdialog.setValue(prog_idx)
|
|
progdialog.setLabelText(str(ca.path))
|
|
center_window_on_parent(progdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
if ca.has_metadata(style) and ca.is_writable():
|
|
if not ca.remove_metadata(style):
|
|
failed_list.append(ca.path)
|
|
else:
|
|
success_count += 1
|
|
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
|
|
progdialog.hide()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.fileSelectionList.update_selected_rows()
|
|
self.update_info_box()
|
|
self.update_menus()
|
|
|
|
summary = f"Successfully removed tags in {success_count} archive(s)."
|
|
if len(failed_list) > 0:
|
|
summary += f"\n\nThe remove operation failed in the following {len(failed_list)} archive(s):\n"
|
|
for f in failed_list:
|
|
summary += f"\t{f}\n"
|
|
|
|
dlg = LogWindow(self)
|
|
dlg.set_text(summary)
|
|
dlg.setWindowTitle("Tag Remove Summary")
|
|
dlg.exec()
|
|
|
|
def copy_tags(self) -> None:
|
|
# copy the indicated tags in the archive
|
|
ca_list = self.fileSelectionList.get_selected_archive_list()
|
|
has_src_count = 0
|
|
|
|
src_style = self.load_data_style
|
|
dest_style = self.save_data_style
|
|
|
|
if src_style == dest_style:
|
|
QtWidgets.QMessageBox.information(
|
|
self, "Copy Tags", "Can't copy tag style onto itself. Read style and modify style must be different."
|
|
)
|
|
return
|
|
|
|
for ca in ca_list:
|
|
if ca.has_metadata(src_style):
|
|
has_src_count += 1
|
|
|
|
if has_src_count == 0:
|
|
QtWidgets.QMessageBox.information(self, "Copy Tags", f"No archives with {src_style} tags selected!")
|
|
return
|
|
|
|
if has_src_count != 0 and not self.dirty_flag_verification(
|
|
"Copy Tags", "If you copy tags now, unsaved data in the form may be lost. Are you sure?"
|
|
):
|
|
return
|
|
|
|
if has_src_count != 0:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
"Copy Tags",
|
|
f"Are you sure you wish to copy the {MetaDataStyle.name[src_style]} tags to {MetaDataStyle.name[dest_style]} tags in {has_src_count} archive(s)?",
|
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
|
QtWidgets.QMessageBox.StandardButton.No,
|
|
)
|
|
|
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
|
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, has_src_count, self)
|
|
prog_dialog.setWindowTitle("Copying Tags")
|
|
prog_dialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
|
|
prog_dialog.setMinimumDuration(300)
|
|
center_window_on_parent(prog_dialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
prog_idx = 0
|
|
|
|
failed_list = []
|
|
success_count = 0
|
|
for ca in ca_list:
|
|
if ca.has_metadata(src_style):
|
|
QtCore.QCoreApplication.processEvents()
|
|
if prog_dialog.wasCanceled():
|
|
break
|
|
prog_idx += 1
|
|
prog_dialog.setValue(prog_idx)
|
|
prog_dialog.setLabelText(str(ca.path))
|
|
center_window_on_parent(prog_dialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
if ca.has_metadata(src_style) and ca.is_writable():
|
|
md = ca.read_metadata(src_style)
|
|
|
|
if dest_style == MetaDataStyle.CBI and self.config[0].cbl_apply_transform_on_bulk_operation:
|
|
md = CBLTransformer(md, self.config[0]).apply()
|
|
|
|
if not ca.write_metadata(md, dest_style):
|
|
failed_list.append(ca.path)
|
|
else:
|
|
success_count += 1
|
|
|
|
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
|
|
prog_dialog.hide()
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.fileSelectionList.update_selected_rows()
|
|
self.update_info_box()
|
|
self.update_menus()
|
|
|
|
summary = f"Successfully copied tags in {success_count} archive(s)."
|
|
if len(failed_list) > 0:
|
|
summary += f"\n\nThe copy operation failed in the following {len(failed_list)} archive(s):\n"
|
|
for f in failed_list:
|
|
summary += f"\t{f}\n"
|
|
|
|
dlg = LogWindow(self)
|
|
dlg.set_text(summary)
|
|
dlg.setWindowTitle("Tag Copy Summary")
|
|
dlg.exec()
|
|
|
|
def actual_issue_data_fetch(self, match: IssueResult) -> GenericMetadata:
|
|
# now get the particular issue data OR series data
|
|
ct_md = GenericMetadata()
|
|
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
|
|
|
try:
|
|
ct_md = self.current_talker().fetch_comic_data(match["issue_id"])
|
|
except TalkerError:
|
|
logger.exception("Save aborted.")
|
|
|
|
if not ct_md.is_empty:
|
|
if self.config[0].cbl_apply_transform_on_import:
|
|
ct_md = CBLTransformer(ct_md, self.config[0]).apply()
|
|
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
|
|
return ct_md
|
|
|
|
def auto_tag_log(self, text: str) -> None:
|
|
IssueIdentifier.default_write_output(text)
|
|
if self.atprogdialog is not None:
|
|
self.atprogdialog.textEdit.append(text.rstrip())
|
|
self.atprogdialog.textEdit.ensureCursorVisible()
|
|
QtCore.QCoreApplication.processEvents()
|
|
QtCore.QCoreApplication.processEvents()
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
def identify_and_tag_single_archive(
|
|
self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow
|
|
) -> tuple[bool, OnlineMatchResults]:
|
|
success = False
|
|
ii = IssueIdentifier(ca, self.config[0], self.current_talker())
|
|
|
|
# read in metadata, and parse file name if not there
|
|
try:
|
|
md = ca.read_metadata(self.save_data_style)
|
|
except Exception as e:
|
|
md = GenericMetadata()
|
|
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
|
if md.is_empty:
|
|
md = ca.metadata_from_filename(
|
|
self.config[0].filename_complicated_parser,
|
|
self.config[0].filename_remove_c2c,
|
|
self.config[0].filename_remove_fcbd,
|
|
self.config[0].filename_remove_publisher,
|
|
dlg.split_words,
|
|
)
|
|
if dlg.ignore_leading_digits_in_filename and md.series is not None:
|
|
# remove all leading numbers
|
|
md.series = re.sub(r"([\d.]*)(.*)", r"\2", md.series)
|
|
|
|
# use the dialog specified search string
|
|
if dlg.search_string:
|
|
md.series = dlg.search_string
|
|
|
|
if md is None or md.is_empty:
|
|
logger.error("No metadata given to search online with!")
|
|
return False, match_results
|
|
|
|
if dlg.dont_use_year:
|
|
md.year = None
|
|
if md.issue is None or md.issue == "":
|
|
if dlg.assume_issue_one:
|
|
md.issue = "1"
|
|
else:
|
|
md.issue = utils.xlate(md.volume)
|
|
ii.set_additional_metadata(md)
|
|
ii.only_use_additional_meta_data = True
|
|
ii.set_output_function(self.auto_tag_log)
|
|
ii.cover_page_index = md.get_cover_page_index_list()[0]
|
|
if self.atprogdialog is not None:
|
|
ii.set_cover_url_callback(self.atprogdialog.set_test_image)
|
|
ii.set_name_series_match_threshold(dlg.name_length_match_tolerance)
|
|
|
|
matches: list[IssueResult] = ii.search()
|
|
|
|
result = ii.search_result
|
|
|
|
found_match = False
|
|
choices = False
|
|
low_confidence = False
|
|
|
|
if result == ii.result_no_matches:
|
|
pass
|
|
elif result == ii.result_found_match_but_bad_cover_score:
|
|
low_confidence = True
|
|
found_match = True
|
|
elif result == ii.result_found_match_but_not_first_page:
|
|
found_match = True
|
|
elif result == ii.result_multiple_matches_with_bad_image_scores:
|
|
low_confidence = True
|
|
choices = True
|
|
elif result == ii.result_one_good_match:
|
|
found_match = True
|
|
elif result == ii.result_multiple_good_matches:
|
|
choices = True
|
|
|
|
if choices:
|
|
if low_confidence:
|
|
self.auto_tag_log("Online search: Multiple low-confidence matches. Save aborted\n")
|
|
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
|
|
else:
|
|
self.auto_tag_log("Online search: Multiple matches. Save aborted\n")
|
|
match_results.multiple_matches.append(MultipleMatch(ca, matches))
|
|
elif low_confidence and not dlg.auto_save_on_low:
|
|
self.auto_tag_log("Online search: Low confidence match. Save aborted\n")
|
|
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
|
|
elif not found_match:
|
|
self.auto_tag_log("Online search: No match found. Save aborted\n")
|
|
match_results.no_matches.append(str(ca.path.absolute()))
|
|
else:
|
|
# a single match!
|
|
if low_confidence:
|
|
self.auto_tag_log("Online search: Low confidence match, but saving anyways, as indicated...\n")
|
|
|
|
# now get the particular issue data
|
|
ct_md = self.actual_issue_data_fetch(matches[0])
|
|
if ct_md is None:
|
|
match_results.fetch_data_failures.append(str(ca.path.absolute()))
|
|
|
|
if ct_md is not None:
|
|
if dlg.cbxRemoveMetadata.isChecked():
|
|
md = ct_md
|
|
else:
|
|
notes = (
|
|
f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on"
|
|
f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {ct_md.issue_id}]"
|
|
)
|
|
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
|
|
|
if self.config[0].identifier_auto_imprint:
|
|
md.fix_publisher()
|
|
|
|
if not ca.write_metadata(md, self.save_data_style):
|
|
match_results.write_failures.append(str(ca.path.absolute()))
|
|
self.auto_tag_log("Save failed ;-(\n")
|
|
else:
|
|
match_results.good_matches.append(str(ca.path.absolute()))
|
|
success = True
|
|
self.auto_tag_log("Save complete!\n")
|
|
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
|
|
|
return success, match_results
|
|
|
|
def auto_tag(self) -> None:
|
|
ca_list = self.fileSelectionList.get_selected_archive_list()
|
|
style = self.save_data_style
|
|
|
|
if len(ca_list) == 0:
|
|
QtWidgets.QMessageBox.information(self, "Auto-Tag", "No archives selected!")
|
|
return
|
|
|
|
if not self.dirty_flag_verification(
|
|
"Auto-Tag", "If you auto-tag now, unsaved data in the form will be lost. Are you sure?"
|
|
):
|
|
return
|
|
|
|
atstartdlg = AutoTagStartWindow(
|
|
self,
|
|
self.config[0],
|
|
(
|
|
f"You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to."
|
|
"\n\nPlease choose config below, and select OK to Auto-Tag."
|
|
),
|
|
)
|
|
|
|
atstartdlg.adjustSize()
|
|
atstartdlg.setModal(True)
|
|
if not atstartdlg.exec():
|
|
return
|
|
|
|
self.atprogdialog = AutoTagProgressWindow(self, self.current_talker())
|
|
self.atprogdialog.setModal(True)
|
|
self.atprogdialog.show()
|
|
self.atprogdialog.progressBar.setMaximum(len(ca_list))
|
|
self.atprogdialog.setWindowTitle("Auto-Tagging")
|
|
|
|
self.auto_tag_log("==========================================================================\n")
|
|
self.auto_tag_log(f"Auto-Tagging Started for {len(ca_list)} items\n")
|
|
|
|
prog_idx = 0
|
|
|
|
match_results = OnlineMatchResults()
|
|
archives_to_remove = []
|
|
for ca in ca_list:
|
|
self.auto_tag_log("==========================================================================\n")
|
|
self.auto_tag_log(f"Auto-Tagging {prog_idx + 1} of {len(ca_list)}\n")
|
|
self.auto_tag_log(f"{ca.path}\n")
|
|
try:
|
|
cover_idx = ca.read_metadata(style).get_cover_page_index_list()[0]
|
|
except Exception as e:
|
|
cover_idx = 0
|
|
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
|
image_data = ca.get_page(cover_idx)
|
|
self.atprogdialog.set_archive_image(image_data)
|
|
self.atprogdialog.set_test_image(b"")
|
|
|
|
QtCore.QCoreApplication.processEvents()
|
|
if self.atprogdialog.isdone:
|
|
break
|
|
self.atprogdialog.progressBar.setValue(prog_idx)
|
|
prog_idx += 1
|
|
self.atprogdialog.label.setText(str(ca.path))
|
|
center_window_on_parent(self.atprogdialog)
|
|
QtCore.QCoreApplication.processEvents()
|
|
|
|
if ca.is_writable():
|
|
success, match_results = self.identify_and_tag_single_archive(ca, match_results, atstartdlg)
|
|
|
|
if success and atstartdlg.remove_after_success:
|
|
archives_to_remove.append(ca)
|
|
|
|
self.atprogdialog.close()
|
|
|
|
if atstartdlg.remove_after_success:
|
|
self.fileSelectionList.remove_archive_list(archives_to_remove)
|
|
self.fileSelectionList.update_selected_rows()
|
|
|
|
new_ca = self.fileSelectionList.get_current_archive()
|
|
if new_ca is not None:
|
|
self.load_archive(new_ca)
|
|
self.atprogdialog = None
|
|
|
|
summary = ""
|
|
summary += f"Successfully tagged archives: {len(match_results.good_matches)}\n"
|
|
|
|
if len(match_results.multiple_matches) > 0:
|
|
summary += f"Archives with multiple matches: {len(match_results.multiple_matches)}\n"
|
|
if len(match_results.low_confidence_matches) > 0:
|
|
summary += (
|
|
f"Archives with one or more low-confidence matches: {len(match_results.low_confidence_matches)}\n"
|
|
)
|
|
if len(match_results.no_matches) > 0:
|
|
summary += f"Archives with no matches: {len(match_results.no_matches)}\n"
|
|
if len(match_results.fetch_data_failures) > 0:
|
|
summary += f"Archives that failed due to data fetch errors: {len(match_results.fetch_data_failures)}\n"
|
|
if len(match_results.write_failures) > 0:
|
|
summary += f"Archives that failed due to file writing errors: {len(match_results.write_failures)}\n"
|
|
|
|
self.auto_tag_log(summary)
|
|
|
|
sum_selectable = len(match_results.multiple_matches) + len(match_results.low_confidence_matches)
|
|
if sum_selectable > 0:
|
|
summary += (
|
|
"\n\nDo you want to manually select the ones with multiple matches and/or low-confidence matches now?"
|
|
)
|
|
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
"Auto-Tag Summary",
|
|
summary,
|
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
|
QtWidgets.QMessageBox.StandardButton.No,
|
|
)
|
|
|
|
match_results.multiple_matches.extend(match_results.low_confidence_matches)
|
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
|
matchdlg = AutoTagMatchWindow(
|
|
self,
|
|
match_results.multiple_matches,
|
|
style,
|
|
self.actual_issue_data_fetch,
|
|
self.config[0],
|
|
self.current_talker(),
|
|
)
|
|
matchdlg.setModal(True)
|
|
matchdlg.exec()
|
|
self.fileSelectionList.update_selected_rows()
|
|
new_ca = self.fileSelectionList.get_current_archive()
|
|
if new_ca is not None:
|
|
self.load_archive(new_ca)
|
|
|
|
else:
|
|
QtWidgets.QMessageBox.information(self, self.tr("Auto-Tag Summary"), self.tr(summary))
|
|
logger.info(summary)
|
|
|
|
def exception(self, message: str) -> None:
|
|
errorbox = QtWidgets.QMessageBox()
|
|
errorbox.setText(message)
|
|
errorbox.exec()
|
|
|
|
def dirty_flag_verification(self, title: str, desc: str) -> bool:
|
|
if self.dirty_flag:
|
|
reply = QtWidgets.QMessageBox.question(
|
|
self,
|
|
title,
|
|
desc,
|
|
(
|
|
QtWidgets.QMessageBox.StandardButton.Save
|
|
| QtWidgets.QMessageBox.StandardButton.Cancel
|
|
| QtWidgets.QMessageBox.StandardButton.Discard
|
|
),
|
|
QtWidgets.QMessageBox.StandardButton.Cancel,
|
|
)
|
|
|
|
if reply == QtWidgets.QMessageBox.StandardButton.Discard:
|
|
return True
|
|
if reply == QtWidgets.QMessageBox.StandardButton.Save:
|
|
self.commit_metadata()
|
|
return True
|
|
return False
|
|
return True
|
|
|
|
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
|
if self.dirty_flag_verification(
|
|
f"Exit {self.appName}", "If you quit now, data in the form will be lost. Are you sure?"
|
|
):
|
|
appsize = self.size()
|
|
self.config[0].internal_window_width = appsize.width()
|
|
self.config[0].internal_window_height = appsize.height()
|
|
self.config[0].internal_window_x = self.x()
|
|
self.config[0].internal_window_y = self.y()
|
|
self.config[0].internal_form_width = self.splitter.sizes()[0]
|
|
self.config[0].internal_list_width = self.splitter.sizes()[1]
|
|
(
|
|
self.config[0].internal_sort_column,
|
|
self.config[0].internal_sort_direction,
|
|
) = self.fileSelectionList.get_sorting()
|
|
settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json")
|
|
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
|
|
def show_page_browser(self) -> None:
|
|
if self.page_browser is None:
|
|
self.page_browser = PageBrowserWindow(self, self.metadata)
|
|
if self.comic_archive is not None:
|
|
self.page_browser.set_comic_archive(self.comic_archive)
|
|
self.page_browser.finished.connect(self.page_browser_closed)
|
|
|
|
def page_browser_closed(self) -> None:
|
|
self.page_browser = None
|
|
|
|
def view_raw_cr_tags(self) -> None:
|
|
if self.comic_archive is not None and self.comic_archive.has_cix():
|
|
dlg = LogWindow(self)
|
|
dlg.set_text(self.comic_archive.read_raw_cix())
|
|
dlg.setWindowTitle("Raw ComicRack Tag View")
|
|
dlg.exec()
|
|
|
|
def view_raw_cbl_tags(self) -> None:
|
|
if self.comic_archive is not None and self.comic_archive.has_cbi():
|
|
dlg = LogWindow(self)
|
|
text = pprint.pformat(json.loads(self.comic_archive.read_raw_cbi()), indent=4)
|
|
dlg.set_text(text)
|
|
dlg.setWindowTitle("Raw ComicBookLover Tag View")
|
|
dlg.exec()
|
|
|
|
def show_wiki(self) -> None:
|
|
webbrowser.open("https://github.com/comictagger/comictagger/wiki")
|
|
|
|
def report_bug(self) -> None:
|
|
webbrowser.open("https://github.com/comictagger/comictagger/issues")
|
|
|
|
def show_forum(self) -> None:
|
|
webbrowser.open("https://github.com/comictagger/comictagger/discussions")
|
|
|
|
def front_cover_changed(self) -> None:
|
|
self.metadata.pages = self.page_list_editor.get_page_list()
|
|
self.update_cover_image()
|
|
|
|
def page_list_order_changed(self) -> None:
|
|
self.metadata.pages = self.page_list_editor.get_page_list()
|
|
|
|
def apply_cbl_transform(self) -> None:
|
|
self.form_to_metadata()
|
|
self.metadata = CBLTransformer(self.metadata, self.config[0]).apply()
|
|
self.metadata_to_form()
|
|
|
|
def recalc_page_dimensions(self) -> None:
|
|
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
|
for p in self.metadata.pages:
|
|
if "ImageSize" in p:
|
|
del p["ImageSize"]
|
|
if "ImageHeight" in p:
|
|
del p["ImageHeight"]
|
|
if "ImageWidth" in p:
|
|
del p["ImageWidth"]
|
|
self.set_dirty_flag()
|
|
QtWidgets.QApplication.restoreOverrideCursor()
|
|
|
|
def rename_archive(self) -> None:
|
|
ca_list = self.fileSelectionList.get_selected_archive_list()
|
|
|
|
if len(ca_list) == 0:
|
|
QtWidgets.QMessageBox.information(self, "Rename", "No archives selected!")
|
|
return
|
|
|
|
if self.dirty_flag_verification(
|
|
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
|
|
):
|
|
dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.talkers)
|
|
dlg.setModal(True)
|
|
if dlg.exec() and self.comic_archive is not None:
|
|
self.fileSelectionList.update_selected_rows()
|
|
self.load_archive(self.comic_archive)
|
|
|
|
def file_list_selection_changed(self, fi: FileInfo) -> None:
|
|
self.load_archive(fi.ca)
|
|
|
|
def load_archive(self, comic_archive: ComicArchive) -> None:
|
|
self.comic_archive = None
|
|
self.clear_form()
|
|
|
|
if not os.path.exists(comic_archive.path):
|
|
self.fileSelectionList.dirty_flag = False
|
|
self.fileSelectionList.remove_archive_list([comic_archive])
|
|
QtCore.QTimer.singleShot(1, self.fileSelectionList.revert_selection)
|
|
return
|
|
|
|
self.config[0].internal_last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
|
|
self.comic_archive = comic_archive
|
|
try:
|
|
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
|
|
except Exception as e:
|
|
logger.error("Failed to load metadata for %s: %s", self.comic_archive.path, e)
|
|
self.exception(f"Failed to load metadata for {self.comic_archive.path}:\n\n{e}")
|
|
self.metadata = GenericMetadata()
|
|
|
|
self.actual_load_current_archive()
|
|
|
|
def file_list_cleared(self) -> None:
|
|
self.reset_app()
|
|
|
|
def splitter_moved_event(self, w1: int, w2: int) -> None:
|
|
scrollbar_w = 0
|
|
if self.scrollArea.verticalScrollBar().isVisible():
|
|
scrollbar_w = self.scrollArea.verticalScrollBar().width()
|
|
|
|
new_w = self.scrollArea.width() - scrollbar_w - 5
|
|
self.scrollAreaWidgetContents.resize(new_w, self.scrollAreaWidgetContents.height())
|
|
|
|
def resizeEvent(self, ev: QtGui.QResizeEvent | None) -> None:
|
|
self.splitter_moved_event(0, 0)
|
|
|
|
def tab_changed(self, idx: int) -> None:
|
|
if idx == 0:
|
|
self.splitter_moved_event(0, 0)
|
|
|
|
def check_latest_version_online(self) -> None:
|
|
version_checker = VersionChecker()
|
|
self.version_check_complete(version_checker.get_latest_version(self.config[0].internal_install_id))
|
|
|
|
def version_check_complete(self, new_version: tuple[str, str]) -> None:
|
|
if new_version[0] not in (self.version, self.config[0].dialog_dont_notify_about_this_version):
|
|
website = "https://github.com/comictagger/comictagger"
|
|
checked = OptionalMessageDialog.msg(
|
|
self,
|
|
"New version available!",
|
|
f"New version ({new_version[1]}) available!<br>(You are currently running {self.version})<br><br>"
|
|
f"Visit <a href='{website}/releases/latest'>{website}/releases/latest</a> for more info.<br><br>",
|
|
False,
|
|
"Don't tell me about this version again",
|
|
)
|
|
if checked:
|
|
self.config[0].dialog_dont_notify_about_this_version = new_version[0]
|
|
|
|
def on_incoming_socket_connection(self) -> None:
|
|
# Accept connection from other instance.
|
|
# Read in the file list if they're giving it, and add to our own list
|
|
local_socket = self.socketServer.nextPendingConnection()
|
|
if local_socket.waitForReadyRead(3000):
|
|
byte_array = local_socket.readAll().data()
|
|
if len(byte_array) > 0:
|
|
obj = pickle.loads(byte_array)
|
|
local_socket.disconnectFromServer()
|
|
if isinstance(obj, list):
|
|
self.fileSelectionList.add_path_list(obj)
|
|
|
|
self.bring_to_top()
|
|
|
|
def bring_to_top(self) -> None:
|
|
if platform.system() == "Windows":
|
|
self.showNormal()
|
|
self.raise_()
|
|
self.activateWindow()
|
|
try:
|
|
import win32con
|
|
import win32gui
|
|
|
|
hwnd = self.effectiveWinId()
|
|
rect = win32gui.GetWindowRect(hwnd)
|
|
x = rect[0]
|
|
y = rect[1]
|
|
w = rect[2] - x
|
|
h = rect[3] - y
|
|
# mark it "always on top", just for a moment, to force it to
|
|
# the top
|
|
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, x, y, w, h, 0)
|
|
win32gui.SetWindowPos(hwnd, win32con.HWND_NOTOPMOST, x, y, w, h, 0)
|
|
except Exception:
|
|
logger.exception("Fail to bring window to top")
|
|
elif platform.system() == "Darwin":
|
|
self.raise_()
|
|
self.showNormal()
|
|
self.activateWindow()
|
|
else:
|
|
flags = QtCore.Qt.WindowType(self.windowFlags())
|
|
self.setWindowFlags(
|
|
flags | QtCore.Qt.WindowType.WindowStaysOnTopHint | QtCore.Qt.WindowType.X11BypassWindowManagerHint
|
|
)
|
|
QtCore.QCoreApplication.processEvents()
|
|
self.setWindowFlags(flags)
|
|
self.show()
|
|
|
|
def auto_imprint(self) -> None:
|
|
self.form_to_metadata()
|
|
self.metadata.fix_publisher()
|
|
self.metadata_to_form()
|