Compare commits

..

No commits in common. "develop" and "original_hash" have entirely different histories.

51 changed files with 214 additions and 350 deletions

@ -1,37 +0,0 @@
---
name: Bug report
about: Report a bug
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Attach logs**
`%LOCALAPPDATA%\ComicTagger\logs` on windows
`~/Library/Logs/ComicTagger` on macOS
`~/.cache/ComicTagger/log` on Linux
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Fedora]
- Version [e.g. 1.6.0b2]
- Where did you install ComicTagger from? [e.g. releases page]
**Additional context**
Add any other context about the problem here.

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature-request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -47,7 +47,7 @@ jobs:
strategy:
matrix:
python-version: [3.9]
os: [ubuntu-22.04, macos-13, macos-14, windows-latest]
os: [ubuntu-22.04, macos-13, windows-latest]
steps:
- uses: actions/checkout@v4
@ -83,7 +83,7 @@ jobs:
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: "${{ format('ComicTagger-{0}', matrix.os) }}"
name: "${{ format('ComicTagger-{0}', runner.os) }}"
path: |
dist/*.whl
dist/binary/*.zip

@ -15,8 +15,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.12]
os: [ubuntu-22.04, macos-13, macos-14, windows-latest]
python-version: [3.9]
os: [ubuntu-22.04, macos-13, windows-latest]
steps:
- uses: actions/checkout@v4

@ -14,7 +14,7 @@ repos:
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
rev: v3.18.0
hooks:
- id: pyupgrade
args: [--py39-plus]
@ -24,7 +24,7 @@ repos:
- id: autoflake
args: [-i, --remove-all-unused-imports, --ignore-init-module-imports]
- repo: https://github.com/PyCQA/isort
rev: 6.0.1
rev: 5.13.2
hooks:
- id: isort
args: [--af,--add-import, 'from __future__ import annotations']
@ -33,14 +33,14 @@ repos:
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.1.2
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [flake8-encodings, flake8-builtins, flake8-print, flake8-no-nested-comprehensions]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
rev: v1.11.2
hooks:
- id: mypy
additional_dependencies: [types-setuptools, types-requests, settngs>=0.10.4, pillow>=9.1.0]
additional_dependencies: [types-setuptools, types-requests, settngs>=0.10.4]
ci:
skip: [mypy]

@ -41,7 +41,7 @@ Please open a [GitHub Pull Request](https://github.com/comictagger/comictagger/p
Currently only python 3.9 is supported however 3.10 will probably work if you try it
Those on linux should install `Pillow` from the system package manager if possible and if the GUI `PyQt6` should be installed from the system package manager
Those on linux should install `Pillow` from the system package manager if possible and if the GUI `pyqt5` should be installed from the system package manager
Those on macOS will need to ensure that you are using python3 in x86 mode either by installing an x86 only version of python or using the universal installer and using `python3-intel64` instead of `python3`

@ -1,6 +1,5 @@
from __future__ import annotations
import os
import pathlib
import settngs
@ -16,8 +15,7 @@ def generate() -> str:
imports2, types2 = settngs.generate_ns(app.manager.definitions)
i = imports.splitlines()
i.extend(set(imports2.splitlines()) - set(i))
os.linesep
return (os.linesep * 2).join((os.linesep.join(i), types2, types))
return "\n\n".join(("\n".join(i), types2, types))
if __name__ == "__main__":

@ -3,16 +3,14 @@ from __future__ import annotations
import argparse
import os
import pathlib
import platform
try:
import niquests as requests
except ImportError:
import requests
arch = platform.machine()
parser = argparse.ArgumentParser()
parser.add_argument("APPIMAGETOOL", default=f"build/appimagetool-{arch}.AppImage", type=pathlib.Path, nargs="?")
parser.add_argument("APPIMAGETOOL", default="build/appimagetool-x86_64.AppImage", type=pathlib.Path, nargs="?")
opts = parser.parse_args()
opts.APPIMAGETOOL = opts.APPIMAGETOOL.absolute()
@ -29,8 +27,7 @@ if opts.APPIMAGETOOL.exists():
raise SystemExit(0)
urlretrieve(
f"https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-{arch}.AppImage",
opts.APPIMAGETOOL,
"https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-x86_64.AppImage", opts.APPIMAGETOOL
)
os.chmod(opts.APPIMAGETOOL, 0o0700)

@ -1,6 +1,5 @@
from __future__ import annotations
import functools
import logging
import os
import pathlib
@ -272,16 +271,10 @@ class RarArchiver(Archiver):
else:
return True
@classmethod
@functools.cache
def _log_not_writeable(cls, exe: str) -> None:
logger.warning("Unable to find a useable copy of %r, will not be able to write rar files", exe)
def is_writable(self) -> bool:
writeable = False
try:
if bool(self.exe and (os.path.exists(self.exe) or shutil.which(self.exe))):
writeable = (
return (
subprocess.run(
(self.exe,),
startupinfo=self.startupinfo,
@ -293,8 +286,6 @@ class RarArchiver(Archiver):
)
except OSError:
...
if not writeable:
self._log_not_writeable(self.exe or "rar")
return False
def extension(self) -> str:

@ -234,7 +234,6 @@ class ComicArchive:
if tag_id in self.md:
del self.md[tag_id]
if not tags[tag_id].enabled:
logger.warning("%s tags not enabled", tags[tag_id].name())
return False
self.apply_archive_info_to_metadata(metadata, True, True, hash_archive=self.hash_archive)
@ -362,7 +361,7 @@ class ComicArchive:
length = digest.name.rpartition("_")[2]
if not length.isdigit():
length = "128"
md.original_hash = FileHash(digest.name, digest.hexdigest(int(length) // 8)) # type: ignore[call-arg]
md.original_hash = FileHash(digest.name, digest.hexdigest(int(length) // 8))
else:
md.original_hash = FileHash(digest.name, digest.hexdigest())
except Exception:

@ -382,7 +382,7 @@ def lex_number(lex: Lexer) -> LexerFunc | None:
return lex_filename
def lex_issue_number(lex: Lexer) -> LexerFunc:
def lex_issue_number(lex: Lexer) -> Callable[[Lexer], Callable | None] | None: # type: ignore[type-arg]
# Only called when lex.input[lex.start] == "#"
original_start = lex.pos
lex.accept_run(str.isalpha)

@ -3,7 +3,7 @@ from __future__ import annotations
import dataclasses
from collections.abc import Collection
from enum import auto
from typing import Any, Callable
from typing import Any
from comicapi.utils import DefaultDict, StrEnum, norm_fold
@ -54,7 +54,7 @@ def overlay(old: Any, new: Any) -> Any:
return new
attribute: DefaultDict[Mode, Callable[[Any, Any], Any]] = DefaultDict(
attribute = DefaultDict(
{
Mode.OVERLAY: overlay,
Mode.ADD_MISSING: lambda old, new: overlay(new, old),
@ -63,7 +63,7 @@ attribute: DefaultDict[Mode, Callable[[Any, Any], Any]] = DefaultDict(
)
lists: DefaultDict[Mode, Callable[[Collection[Any], Collection[Any]], list[Any] | set[Any]]] = DefaultDict(
lists = DefaultDict(
{
Mode.OVERLAY: merge_lists,
Mode.ADD_MISSING: lambda old, new: merge_lists(new, old),

@ -23,7 +23,7 @@ import pathlib
import platform
import sys
import unicodedata
from collections.abc import Iterable, Mapping, Sequence
from collections.abc import Iterable, Mapping
from enum import Enum, auto
from shutil import which # noqa: F401
from typing import Any, Callable, TypeVar, cast
@ -47,7 +47,7 @@ except ImportError:
if sys.version_info < (3, 11):
def file_digest(fileobj, digest, /, *, _bufsize=2**18): # type: ignore[no-untyped-def]
def file_digest(fileobj, digest, /, *, _bufsize=2**18):
"""Hash the contents of a file-like object. Returns a digest object.
*fileobj* must be a file-like object opened for reading in binary mode.
@ -147,16 +147,12 @@ else:
logger = logging.getLogger(__name__)
_KT = TypeVar("_KT")
_VT = TypeVar("_VT")
class DefaultDict(dict[_KT, _VT]):
def __init__(self, *args, default: Callable[[_KT], _VT | _KT] | None = None, **kwargs) -> None: # type: ignore[no-untyped-def]
super().__init__(*args, **kwargs)
class DefaultDict(dict):
def __init__(self, *args, default: Callable[[Any], Any] | None = None) -> None:
super().__init__(*args)
self.default = default
def __missing__(self, key: _KT) -> _VT | _KT:
def __missing__(self, key: Any) -> Any:
if self.default is None:
return key
return self.default(key)
@ -174,7 +170,7 @@ def _custom_key(tup: Any) -> Any:
lst = []
for x in natsort.os_sort_keygen()(tup):
ret = x
if isinstance(x, Sequence) and len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "":
if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "":
ret = ("a", *x[1:])
lst.append(ret)
@ -627,7 +623,7 @@ def update_publishers(new_publishers: Mapping[str, Mapping[str, str]]) -> None:
publishers[publisher] = ImprintDict(publisher, new_publishers[publisher])
class ImprintDict(dict[str, str]):
class ImprintDict(dict): # type: ignore
"""
ImprintDict takes a publisher and a dict or mapping of lowercased
imprint names to the proper imprint name. Retrieving a value from an
@ -635,14 +631,14 @@ class ImprintDict(dict[str, str]):
if the key does not exist the key is returned as the publisher unchanged
"""
def __init__(self, publisher: str, mapping: Mapping[str, str] = {}, **kwargs) -> None: # type: ignore[no-untyped-def]
def __init__(self, publisher: str, mapping: tuple | Mapping = (), **kwargs: dict) -> None: # type: ignore
super().__init__(mapping, **kwargs)
self.publisher = publisher
def __missing__(self, key: str) -> None:
return None
def __getitem__(self, k: str) -> tuple[str, str, bool]: # type: ignore[override]
def __getitem__(self, k: str) -> tuple[str, str, bool]:
item = super().__getitem__(k.casefold())
if k.casefold() == self.publisher.casefold():
return "", self.publisher, True

@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import pathlib
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.ui import ui_path
@ -35,7 +35,7 @@ class ApplicationLogWindow(QtWidgets.QDialog):
self.log_handler.qlog.connect(self.textEdit.append)
f = QtGui.QFont("menlo")
f.setStyleHint(QtGui.QFont.StyleHint.Monospace)
f.setStyleHint(QtGui.QFont.Monospace)
self.setFont(f)
self._button = QtWidgets.QPushButton(self)
self._button.setText("Test Me")

@ -20,7 +20,7 @@ import logging
import os
from typing import Callable
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, tags
from comicapi.genericmetadata import GenericMetadata

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ui import ui_path

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.ui import ui_path

@ -121,7 +121,7 @@ class CLI:
self.post_process_matches(match_results)
if self.config.Auto_Tag__online and results and results[-1].online_results:
if self.config.Auto_Tag__online and results and results[-1].online_results != []:
self.output(
f"\nFiles tagged with metadata provided by {self.current_talker().name} {self.current_talker().website}",
)
@ -646,7 +646,7 @@ class CLI:
url_image_hash=-1,
issue_title=ct_md.title or "",
issue_id=ct_md.issue_id or "",
series_id=ct_md.series_id or "",
series_id=ct_md.issue_id or "",
month=ct_md.month,
year=ct_md.year,
publisher=None,

@ -1,4 +1,4 @@
"""A PyQt6 widget to display cover images
"""A PyQt5 widget to display cover images
Display cover images from either a local archive, or from comic source metadata.
TODO: This should be re-factored using subclasses!
@ -23,7 +23,7 @@ from __future__ import annotations
import logging
import pathlib
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.imagefetcher import ImageFetcher

@ -20,7 +20,7 @@ import logging
import operator
import natsort
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import Credit

@ -26,7 +26,7 @@ import subprocess
import settngs
from comicapi import comicarchive, utils
from comicapi import utils
from comicapi.comicarchive import tags
from comictaggerlib import ctversion, quick_tag
from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns
@ -283,15 +283,6 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
+ "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n",
)
enabled_tags = {tag for tag in comicarchive.tags if comicarchive.tags[tag].enabled}
if (
(not config[0].Metadata_Options__cr)
and "cr" in comicarchive.tags
and comicarchive.tags["cr"].enabled
and len(enabled_tags) > 1
):
comicarchive.tags["cr"].enabled = False
config[0].Runtime_Options__no_gui = any(
(config[0].Commands__command != Action.gui, config[0].Runtime_Options__no_gui, config[0].Commands__copy)
)
@ -311,28 +302,6 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
if config[0].Runtime_Options__tags_read and not config[0].Runtime_Options__tags_write:
config[0].Runtime_Options__tags_write = config[0].Runtime_Options__tags_read
disabled_tags = {tag for tag in comicarchive.tags if not comicarchive.tags[tag].enabled}
to_be_removed = (
set(config[0].Runtime_Options__tags_read)
.union(config[0].Runtime_Options__tags_write)
.intersection(disabled_tags)
)
if to_be_removed:
logger.debug("Removing disabled tags: %s", to_be_removed)
config[0].Runtime_Options__tags_read = [
tag for tag in config[0].Runtime_Options__tags_read if tag not in to_be_removed
]
config[0].Runtime_Options__tags_write = [
tag for tag in config[0].Runtime_Options__tags_write if tag not in to_be_removed
]
if (
config[0].Runtime_Options__no_gui
and not [tag.id for tag in tags.values() if tag.enabled]
and config[0].Commands__command != Action.list_plugins
):
parser.exit(status=1, message="There are no tags enabled see --list-plugins\n")
if config[0].Runtime_Options__no_gui and not config[0].Runtime_Options__files:
if config[0].Commands__command == Action.print and not config[0].Auto_Tag__metadata.is_empty:
... # allow printing the metadata provided on the commandline

@ -10,7 +10,7 @@ import pathlib
import platform
import re
import sys
from collections.abc import Generator, Iterable, Sequence
from collections.abc import Generator, Iterable
from typing import Any, NamedTuple, TypeVar
if sys.version_info < (3, 10):
@ -31,7 +31,7 @@ def _custom_key(tup: Any) -> Any:
lst = []
for x in natsort.os_sort_keygen()(tup):
ret = x
if isinstance(x, Sequence) and len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "":
if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "":
ret = ("a", *x[1:])
lst.append(ret)
@ -142,7 +142,6 @@ def find_plugins(plugin_folder: pathlib.Path) -> Plugins:
for plugin_path in os_sorted(zips):
logger.debug("looking for plugins in %s", plugin_path)
sys_path = sys.path.copy()
try:
sys.path.append(str(plugin_path))
for plugin in _find_local_plugins(plugin_path):
@ -151,7 +150,7 @@ def find_plugins(plugin_folder: pathlib.Path) -> Plugins:
except Exception as err:
logger.exception(FailedToLoadPlugin(plugin_path.name, err))
finally:
sys.path = sys_path
sys.path.remove(str(plugin_path))
for mod in list(sys.modules.values()):
if (
mod is not None

@ -152,12 +152,11 @@ class ComicTaggerPaths(AppDirs):
def tag(types: str) -> list[str]:
enabled_tags = [tag for tag in tags if tags[tag].enabled]
result = []
types = types.casefold()
for typ in utils.split(types, ","):
if typ not in enabled_tags:
choices = ", ".join(enabled_tags)
if typ not in tags:
choices = ", ".join(tags)
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
result.append(tags[typ].id)
return result

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.ui import ui_path

@ -1,4 +1,4 @@
"""A PyQt6 widget for managing list of comic archive files"""
"""A PyQt5 widget for managing list of comic archive files"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -21,7 +21,7 @@ import os
import platform
from typing import Callable, cast
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive
@ -63,9 +63,9 @@ class FileSelectionList(QtWidgets.QWidget):
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
self.dirty_flag = False
select_all_action = QtGui.QAction("Select All", self)
remove_action = QtGui.QAction("Remove Selected Items", self)
self.separator = QtGui.QAction("", self)
select_all_action = QtWidgets.QAction("Select All", self)
remove_action = QtWidgets.QAction("Remove Selected Items", self)
self.separator = QtWidgets.QAction("", self)
self.separator.setSeparator(True)
select_all_action.setShortcut("Ctrl+A")
@ -83,14 +83,14 @@ class FileSelectionList(QtWidgets.QWidget):
def get_sorting(self) -> tuple[int, int]:
col = self.twList.horizontalHeader().sortIndicatorSection()
order = self.twList.horizontalHeader().sortIndicatorOrder().value
order = self.twList.horizontalHeader().sortIndicatorOrder()
return int(col), int(order)
def set_sorting(self, col: int, order: QtCore.Qt.SortOrder) -> None:
self.twList.horizontalHeader().setSortIndicator(col, order)
def add_app_action(self, action: QtGui.QAction) -> None:
self.insertAction(QtGui.QAction(), action)
def add_app_action(self, action: QtWidgets.QAction) -> None:
self.insertAction(QtWidgets.QAction(), action)
def set_modified_flag(self, modified: bool) -> None:
self.dirty_flag = modified

@ -2,11 +2,11 @@
# Resource object code
#
# Created by: The Resource Compiler for PyQt6 (Qt v5.15.11)
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.11)
#
# WARNING! All changes made in this file will be lost!
from PyQt6 import QtCore
from PyQt5 import QtCore
qt_resource_data = b"\
\x00\x00\x16\x0b\

@ -16,7 +16,7 @@ from comictalker.comictalker import ComicTalker
logger = logging.getLogger("comictagger")
try:
qt_available = True
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt5 import QtCore, QtGui, QtWidgets
def show_exception_box(log_msg: str) -> None:
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
@ -70,7 +70,7 @@ try:
try:
# needed here to initialize QWebEngine
from PyQt6.QtWebEngineWidgets import QWebEngineView # noqa: F401
from PyQt5.QtWebEngineWidgets import QWebEngineView # noqa: F401
qt_webengine_available = True
except ImportError:
@ -81,8 +81,8 @@ try:
# Handles "Open With" from Finder on macOS
def event(self, event: QtCore.QEvent) -> bool:
if event.type() == QtCore.QEvent.Type.FileOpen:
logger.info("file open recieved: %s", event.url().toLocalFile())
if event.type() == QtCore.QEvent.FileOpen:
logger.info(event.url().toLocalFile())
self.openFileRequest.emit(event.url())
return True
return super().event(event)

@ -33,7 +33,7 @@ except ImportError:
from comictaggerlib import ctversion
if TYPE_CHECKING:
from PyQt6 import QtCore, QtNetwork
from PyQt5 import QtCore, QtNetwork
logger = logging.getLogger(__name__)
@ -57,7 +57,7 @@ class ImageFetcher:
if self.qt_available:
try:
from PyQt6 import QtNetwork
from PyQt5 import QtNetwork
self.qt_available = True
except ImportError:
@ -99,7 +99,7 @@ class ImageFetcher:
return image_data
if self.qt_available:
from PyQt6 import QtCore, QtNetwork
from PyQt5 import QtCore, QtNetwork
# if we found it, just emit the signal asap
if image_data:

@ -35,12 +35,7 @@ logger = logging.getLogger(__name__)
class ImageHasher:
def __init__(
self,
path: str | None = None,
image: Image.Image | None = None,
data: bytes = b"",
width: int = 8,
height: int = 8,
self, path: str | None = None, image: Image | None = None, data: bytes = b"", width: int = 8, height: int = 8
) -> None:
self.width = width
self.height = height
@ -146,7 +141,6 @@ class ImageHasher:
row = []
for x in range(width):
pixel = image.getpixel((x, y))
assert isinstance(pixel, float)
row.append(pixel)
pixels2.append(row)

@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import platform
from PyQt6 import QtCore, QtGui, QtWidgets, sip, uic
from PyQt5 import QtCore, QtGui, QtWidgets, sip, uic
from comictaggerlib.ui import ui_path
@ -82,7 +82,7 @@ class ImagePopup(QtWidgets.QDialog):
screen_size.width(),
screen_size.height(),
QtCore.Qt.AspectRatioMode.IgnoreAspectRatio,
QtCore.Qt.TransformationMode.SmoothTransformation,
QtCore.Qt.SmoothTransformation,
)
self.setMask(self.clientBgPixmap.mask())
@ -104,10 +104,7 @@ class ImagePopup(QtWidgets.QDialog):
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
# scale the pixmap to fit in the frame
display_pixmap = self.imagePixmap.scaled(
win_w,
win_h,
QtCore.Qt.AspectRatioMode.KeepAspectRatio,
QtCore.Qt.TransformationMode.SmoothTransformation,
win_w, win_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.SmoothTransformation
)
self.lblImage.setPixmap(display_pixmap)
else:

@ -132,7 +132,7 @@ class IssueIdentifier:
def set_cover_url_callback(self, cb_func: Callable[[bytes], None]) -> None:
self.cover_url_callback = cb_func
def calculate_hash(self, image_data: bytes = b"", image: Image.Image | None = None) -> int:
def calculate_hash(self, image_data: bytes = b"", image: Image = None) -> int:
if self.image_hasher == 3:
return ImageHasher(data=image_data, image=image).p_hash()
if self.image_hasher == 2:
@ -377,8 +377,8 @@ class IssueIdentifier:
def _process_cover(self, name: str, image_data: bytes) -> list[tuple[str, Image.Image]]:
assert Image
cover_image: Image.Image = Image.open(io.BytesIO(image_data))
images: list[tuple[str, Image.Image]] = [(name, cover_image)]
cover_image = Image.open(io.BytesIO(image_data))
images = [(name, cover_image)]
# check the aspect ratio
# if it's wider than it is high, it's probably a two page spread (back_cover, front_cover)
@ -413,7 +413,7 @@ class IssueIdentifier:
def _get_search_keys(self, md: GenericMetadata) -> Any:
search_keys = SearchKeys(
series=md.series or "",
series=md.series,
issue_number=IssueString(md.issue).as_string(),
alternate_number=IssueString(md.alternate_number).as_string(),
month=md.month,

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.ui import qtutils, ui_path

@ -246,6 +246,13 @@ class App:
# config already loaded
error = None
if (
not self.config[0].Metadata_Options__cr
and "cr" in comicapi.comicarchive.tags
and comicapi.comicarchive.tags["cr"].enabled
):
comicapi.comicarchive.tags["cr"].enabled = False
if len(self.talkers) < 1:
error = (
"Failed to load any talkers, please re-install and check the log located in '"
@ -296,7 +303,7 @@ class App:
return gui.open_tagger_window(self.talkers, self.config, error)
except ImportError:
self.config[0].Runtime_Options__no_gui = True
logger.warning("PyQt6 is not available. ComicTagger is limited to command-line mode.")
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
# GUI mode is not available or CLI mode was requested
if error and error[1]:

@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import os
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.coverimagewidget import CoverImageWidget

@ -1,4 +1,4 @@
"""A PyQt6 dialog to show a message and let the user check a box
"""A PyQt5 dialog to show a message and let the user check a box
Example usage:
@ -29,7 +29,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtWidgets
from PyQt5 import QtCore, QtWidgets
logger = logging.getLogger(__name__)

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata

@ -1,4 +1,4 @@
"""A PyQt6 widget for editing the page list info"""
"""A PyQt5 widget for editing the page list info"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, tags
from comicapi.genericmetadata import GenericMetadata, PageMetadata, PageType
@ -145,7 +145,7 @@ class PageListEditor(QtWidgets.QWidget):
if show_shortcut:
text = text + " (" + shortcut + ")"
self.cbPageType.addItem(text, user_data)
action_item = QtGui.QAction(shortcut, self)
action_item = QtWidgets.QAction(shortcut, self)
action_item.triggered.connect(lambda: self.select_page_type_item(self.cbPageType.findData(user_data)))
action_item.setShortcut(shortcut)
self.addAction(action_item)

@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore
from PyQt5 import QtCore
from comicapi.comicarchive import ComicArchive

@ -1,4 +1,4 @@
"""A PyQt6 dialog to show ID log and progress"""
"""A PyQt5 dialog to show ID log and progress"""
#
# Copyright 2012-2014 ComicTagger Authors
@ -18,7 +18,7 @@ from __future__ import annotations
import logging
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.ui import ui_path

@ -19,7 +19,7 @@ from __future__ import annotations
import logging
import settngs
from PyQt6 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive, tags

@ -21,8 +21,8 @@ import logging
from collections import deque
import natsort
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt6.QtCore import QUrl, pyqtSignal
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import QUrl, pyqtSignal
from comicapi import utils
from comicapi.comicarchive import ComicArchive

@ -26,7 +26,7 @@ import urllib.parse
from typing import Any, cast
import settngs
from PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
import comictaggerlib.ui.talkeruigenerator
from comicapi import merge, utils

@ -31,7 +31,7 @@ from typing import Any, Callable, cast
import natsort
import settngs
from PyQt6 import QtCore, QtGui, QtNetwork, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
import comicapi.merge
import comictaggerlib.graphics.resources
@ -234,8 +234,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
if self.config[0].Runtime_Options__preferred_hash:
self.config[0].internal__embedded_hash_type = self.config[0].Runtime_Options__preferred_hash
self.selected_write_tags: list[str] = config[0].internal__write_tags
self.selected_read_tags: list[str] = config[0].internal__read_tags
self.selected_write_tags: list[str] = config[0].internal__write_tags or [self.enabled_tags()[0]]
self.selected_read_tags: list[str] = config[0].internal__read_tags or [self.enabled_tags()[0]]
self.setAcceptDrops(True)
self.view_tag_actions, self.remove_tag_actions = self.tag_actions()
@ -352,27 +352,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
""",
)
self.config[0].Dialog_Flags__notify_plugin_changes = not checked
if self.enabled_tags():
self.selected_write_tags = [self.enabled_tags()[0]]
self.selected_read_tags = [self.enabled_tags()[0]]
else:
checked = OptionalMessageDialog.msg_no_checkbox(
self,
"No tags enabled",
"""
There are no tags enabled!<br/><br/>
Go to the "Metadata Options" tab in settings to enable the builtin "Comic Rack" tags
""",
)
if self.config[0].General__check_for_new_version:
self.check_latest_version_online()
def enabled_tags(self) -> Sequence[str]:
return [tag.id for tag in tags.values() if tag.enabled]
def tag_actions(self) -> tuple[dict[str, QtGui.QAction], dict[str, QtGui.QAction]]:
view_raw_tags: dict[str, QtGui.QAction] = {}
remove_raw_tags: dict[str, QtGui.QAction] = {}
def tag_actions(self) -> tuple[dict[str, QtWidgets.QAction], dict[str, QtWidgets.QAction]]:
view_raw_tags: dict[str, QtWidgets.QAction] = {}
remove_raw_tags: dict[str, QtWidgets.QAction] = {}
for tag in tags.values():
view_raw_tags[tag.id] = self.menuViewRawTags.addAction(f"View Raw {tag.name()} Tags")
view_raw_tags[tag.id].setEnabled(tag.enabled)
@ -449,12 +438,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
def toggle_enable_embedding_hashes(self) -> None:
self.config[0].Runtime_Options__enable_embedding_hashes = self.actionEnableEmbeddingHashes.isChecked()
enabled_widgets = set()
for tag_id in self.selected_write_tags:
if not tags[tag_id].enabled:
continue
enabled_widgets.update(tags[tag_id].supported_attributes)
enable_widget(self.md_attributes["original_hash"], self.config[0].Runtime_Options__enable_embedding_hashes and 'original_hash' in enabled_widgets)
enable_widget(self.md_attributes["original_hash"], self.config[0].Runtime_Options__enable_embedding_hashes)
if not self.leOriginalHash.text().strip():
self.cbHashName.setCurrentText(self.config[0].internal__embedded_hash_type)
if self.config[0].Runtime_Options__enable_embedding_hashes:
@ -485,7 +469,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.actionParse_Filename_split_words.triggered.connect(self.use_filename_split)
self.actionReCalcArchiveInfo.triggered.connect(self.recalc_archive_info)
self.actionSearchOnline.triggered.connect(self.query_online)
self.actionEnableEmbeddingHashes: QtGui.QAction
self.actionEnableEmbeddingHashes: QtWidgets.QAction
self.actionEnableEmbeddingHashes.triggered.connect(self.toggle_enable_embedding_hashes)
self.actionEnableEmbeddingHashes.setChecked(self.config[0].Runtime_Options__enable_embedding_hashes)
# Window Menu

@ -6,8 +6,8 @@ from enum import auto
from sys import platform
from typing import Any
from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt, pyqtSignal
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt, pyqtSignal
from comicapi.utils import StrEnum
@ -30,41 +30,41 @@ class ModifyStyleItemDelegate(QtWidgets.QStyledItemDelegate):
# Draw background with the same color as other widgets
palette = self.combobox.palette()
background_color = palette.color(QtGui.QPalette.ColorRole.Window)
background_color = palette.color(QtGui.QPalette.Window)
painter.fillRect(options.rect, background_color)
style.drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_PanelItemViewItem, options, painter, self.combobox)
style.drawPrimitive(QtWidgets.QStyle.PE_PanelItemViewItem, options, painter, self.combobox)
painter.save()
# Checkbox drawing logic
checked = index.data(Qt.ItemDataRole.CheckStateRole)
checked = index.data(Qt.CheckStateRole)
opts = QtWidgets.QStyleOptionButton()
opts.state |= QtWidgets.QStyle.StateFlag.State_Active
opts.state |= QtWidgets.QStyle.State_Active
opts.rect = self.getCheckBoxRect(options)
opts.state |= QtWidgets.QStyle.StateFlag.State_ReadOnly
opts.state |= QtWidgets.QStyle.State_ReadOnly
if checked:
opts.state |= QtWidgets.QStyle.StateFlag.State_On
opts.state |= QtWidgets.QStyle.State_On
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opts, painter, self.combobox
)
else:
opts.state |= QtWidgets.QStyle.StateFlag.State_Off
opts.state |= QtWidgets.QStyle.State_Off
if platform != "darwin":
style.drawControl(QtWidgets.QStyle.ControlElement.CE_CheckBox, opts, painter, self.combobox)
style.drawControl(QtWidgets.QStyle.CE_CheckBox, opts, painter, self.combobox)
label = index.data(Qt.ItemDataRole.DisplayRole)
label = index.data(Qt.DisplayRole)
rectangle = options.rect
rectangle.setX(opts.rect.width() + 10)
# We need the restore here so that text is colored properly
painter.drawText(rectangle, Qt.AlignVCenter, label)
painter.restore()
painter.drawText(rectangle, Qt.AlignmentFlag.AlignVCenter, label)
def getCheckBoxRect(self, option: QtWidgets.QStyleOptionViewItem) -> QRect:
# Get size of a standard checkbox.
opts = QtWidgets.QStyleOptionButton()
style = option.widget.style()
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SubElement.SE_CheckBoxIndicator, opts, None)
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SE_CheckBoxIndicator, opts, None)
y = option.rect.y()
h = option.rect.height()
checkBoxTopLeftCorner = QPoint(5, int(y + h / 2 - checkBoxRect.height() / 2))
@ -99,23 +99,23 @@ class CheckableComboBox(QtWidgets.QComboBox):
# https://stackoverflow.com/questions/65826378/how-do-i-use-qcombobox-setplaceholdertext/65830989#65830989
def paintEvent(self, event: QEvent) -> None:
painter = QtWidgets.QStylePainter(self)
painter.setPen(self.palette().color(QtGui.QPalette.ColorRole.Text))
painter.setPen(self.palette().color(QtGui.QPalette.Text))
# draw the combobox frame, focusrect and selected etc.
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
painter.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_ComboBox, opt)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
if self.currentIndex() < 0:
opt.palette.setBrush(
QtGui.QPalette.ColorRole.ButtonText,
opt.palette.brush(QtGui.QPalette.ColorRole.ButtonText).color(),
QtGui.QPalette.ButtonText,
opt.palette.brush(QtGui.QPalette.ButtonText).color(),
)
if self.placeholderText():
opt.currentText = self.placeholderText()
# draw the icon and text
painter.drawControl(QtWidgets.QStyle.ControlElement.CE_ComboBoxLabel, opt)
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt)
def resizeEvent(self, event: Any) -> None:
# Recompute text to elide as needed
@ -126,16 +126,16 @@ class CheckableComboBox(QtWidgets.QComboBox):
# Allow events before the combobox list is shown
if obj == self.view().viewport():
# We record that the combobox list has been shown
if event.type() == QEvent.Type.Show:
if event.type() == QEvent.Show:
self.justShown = True
# We record that the combobox list has hidden,
# this will happen if the user does not make a selection
# but clicks outside of the combobox list or presses escape
if event.type() == QEvent.Type.Hide:
if event.type() == QEvent.Hide:
self._updateText()
self.justShown = False
# QEvent.Type.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.Type.MouseButtonRelease:
# QEvent.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.MouseButtonRelease:
# If self.justShown is true it means that they clicked on the combobox to change the checked items
# This is standard behavior (on macos) but I think it is surprising when it has a multiple select
if self.justShown:
@ -153,7 +153,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
res = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.CheckState.Checked:
if item.checkState() == Qt.Checked:
res.append(self.itemData(i))
return res
@ -168,7 +168,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
texts = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.CheckState.Checked:
if item.checkState() == Qt.Checked:
texts.append(item.text())
text = ", ".join(texts)
@ -180,24 +180,22 @@ class CheckableComboBox(QtWidgets.QComboBox):
so.initFrom(self)
# Ask the style for the size of the text field
rect = self.style().subControlRect(
QtWidgets.QStyle.ComplexControl.CC_ComboBox, so, QtWidgets.QStyle.SubControl.SC_ComboBoxEditField
)
rect = self.style().subControlRect(QtWidgets.QStyle.CC_ComboBox, so, QtWidgets.QStyle.SC_ComboBoxEditField)
# Compute the elided text
elidedText = self.fontMetrics().elidedText(text, Qt.TextElideMode.ElideRight, rect.width())
elidedText = self.fontMetrics().elidedText(text, Qt.ElideRight, rect.width())
# This CheckableComboBox does not use the index, so we clear it and set the placeholder text
self.setCurrentIndex(-1)
self.setPlaceholderText(elidedText)
def setItemChecked(self, index: Any, state: bool) -> None:
qt_state = Qt.CheckState.Checked if state else Qt.CheckState.Unchecked
qt_state = Qt.Checked if state else Qt.Unchecked
item = self.model().item(index)
current = self.currentData()
# If we have at least one item checked emit itemChecked with the current check state and update text
# Require at least one item to be checked and provide a tooltip
if len(current) == 1 and not state and item.checkState() == Qt.CheckState.Checked:
if len(current) == 1 and not state and item.checkState() == Qt.Checked:
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), self.toolTip(), self, QRect(), 3000)
return
@ -207,7 +205,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
self._updateText()
def toggleItem(self, index: int) -> None:
if self.model().item(index).checkState() == Qt.CheckState.Checked:
if self.model().item(index).checkState() == Qt.Checked:
self.setItemChecked(index, False)
else:
self.setItemChecked(index, True)
@ -242,44 +240,44 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
# Draw background with the same color as other widgets
palette = self.combobox.palette()
background_color = palette.color(QtGui.QPalette.ColorRole.Window)
background_color = palette.color(QtGui.QPalette.Window)
painter.fillRect(options.rect, background_color)
style.drawPrimitive(QtWidgets.QStyle.PrimitiveElement.PE_PanelItemViewItem, options, painter, self.combobox)
style.drawPrimitive(QtWidgets.QStyle.PE_PanelItemViewItem, options, painter, self.combobox)
painter.save()
# Checkbox drawing logic
checked = index.data(Qt.ItemDataRole.CheckStateRole)
checked = index.data(Qt.CheckStateRole)
opts = QtWidgets.QStyleOptionButton()
opts.state |= QtWidgets.QStyle.StateFlag.State_Active
opts.state |= QtWidgets.QStyle.State_Active
opts.rect = self.getCheckBoxRect(options)
opts.state |= QtWidgets.QStyle.StateFlag.State_ReadOnly
opts.state |= QtWidgets.QStyle.State_ReadOnly
if checked:
opts.state |= QtWidgets.QStyle.StateFlag.State_On
opts.state |= QtWidgets.QStyle.State_On
style.drawPrimitive(
QtWidgets.QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opts, painter, self.combobox
)
else:
opts.state |= QtWidgets.QStyle.StateFlag.State_Off
opts.state |= QtWidgets.QStyle.State_Off
if platform != "darwin":
style.drawControl(QtWidgets.QStyle.ControlElement.CE_CheckBox, opts, painter, self.combobox)
style.drawControl(QtWidgets.QStyle.CE_CheckBox, opts, painter, self.combobox)
label = index.data(Qt.ItemDataRole.DisplayRole)
label = index.data(Qt.DisplayRole)
rectangle = options.rect
rectangle.setX(opts.rect.width() + 10)
# We need the restore here so that text is colored properly
painter.restore()
painter.drawText(rectangle, Qt.AlignmentFlag.AlignVCenter, label)
painter.drawText(rectangle, Qt.AlignVCenter, label)
# Draw buttons
if checked and (options.state & QtWidgets.QStyle.StateFlag.State_Selected):
if checked and (options.state & QtWidgets.QStyle.State_Selected):
up_rect = self._button_up_rect(options.rect)
down_rect = self._button_down_rect(options.rect)
painter.drawImage(up_rect, self.up_icon)
painter.drawImage(down_rect, self.down_icon)
painter.restore()
def _button_up_rect(self, rect: QRect) -> QRect:
return QRect(
self.combobox.view().width() - (self.button_width * 2) - (self.button_padding * 2),
@ -300,7 +298,7 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
# Get size of a standard checkbox.
opts = QtWidgets.QStyleOptionButton()
style = option.widget.style()
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SubElement.SE_CheckBoxIndicator, opts, None)
checkBoxRect = style.subElementRect(QtWidgets.QStyle.SE_CheckBoxIndicator, opts, None)
y = option.rect.y()
h = option.rect.height()
checkBoxTopLeftCorner = QPoint(5, int(y + h / 2 - checkBoxRect.height() / 2))
@ -309,7 +307,7 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
def itemClicked(self, index: QModelIndex, pos: QPoint) -> None:
item_rect = self.combobox.view().visualRect(index)
checked = index.data(Qt.ItemDataRole.CheckStateRole)
checked = index.data(Qt.CheckStateRole)
button_up_rect = self._button_up_rect(item_rect)
button_down_rect = self._button_down_rect(item_rect)
@ -338,11 +336,11 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
item_rect = view.visualRect(index)
button_up_rect = self._button_up_rect(item_rect)
button_down_rect = self._button_down_rect(item_rect)
checked = index.data(Qt.ItemDataRole.CheckStateRole)
checked = index.data(Qt.CheckStateRole)
if checked == Qt.CheckState.Checked and button_up_rect.contains(event.pos()):
if checked == Qt.Checked and button_up_rect.contains(event.pos()):
QtWidgets.QToolTip.showText(event.globalPos(), self.up_help, self.combobox, QRect(), 3000)
elif checked == Qt.CheckState.Checked and button_down_rect.contains(event.pos()):
elif checked == Qt.Checked and button_down_rect.contains(event.pos()):
QtWidgets.QToolTip.showText(event.globalPos(), self.down_help, self.combobox, QRect(), 3000)
else:
QtWidgets.QToolTip.showText(event.globalPos(), self.item_help, self.combobox, QRect(), 3000)
@ -382,23 +380,23 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
# https://stackoverflow.com/questions/65826378/how-do-i-use-qcombobox-setplaceholdertext/65830989#65830989
def paintEvent(self, event: QEvent) -> None:
painter = QtWidgets.QStylePainter(self)
painter.setPen(self.palette().color(QtGui.QPalette.ColorRole.Text))
painter.setPen(self.palette().color(QtGui.QPalette.Text))
# draw the combobox frame, focusrect and selected etc.
opt = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(opt)
painter.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_ComboBox, opt)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, opt)
if self.currentIndex() < 0:
opt.palette.setBrush(
QtGui.QPalette.ColorRole.ButtonText,
opt.palette.brush(QtGui.QPalette.ColorRole.ButtonText).color(),
QtGui.QPalette.ButtonText,
opt.palette.brush(QtGui.QPalette.ButtonText).color(),
)
if self.placeholderText():
opt.currentText = self.placeholderText()
# draw the icon and text
painter.drawControl(QtWidgets.QStyle.ControlElement.CE_ComboBoxLabel, opt)
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, opt)
def buttonClicked(self, index: QModelIndex, button: ClickedButtonEnum) -> None:
if button == ClickedButtonEnum.up:
@ -417,18 +415,18 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
# Allow events before the combobox list is shown
if obj == self.view().viewport():
# We record that the combobox list has been shown
if event.type() == QEvent.Type.Show:
if event.type() == QEvent.Show:
self.justShown = True
# We record that the combobox list has hidden,
# this will happen if the user does not make a selection
# but clicks outside of the combobox list or presses escape
if event.type() == QEvent.Type.Hide:
if event.type() == QEvent.Hide:
self._updateText()
self.justShown = False
# Reverse as the display order is in "priority" order for the user whereas overlay requires reversed
self.dropdownClosed.emit(self.currentData())
# QEvent.Type.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.Type.MouseButtonRelease:
# QEvent.MouseButtonPress is inconsistent on activation because double clicks are a thing
if event.type() == QEvent.MouseButtonRelease:
# If self.justShown is true it means that they clicked on the combobox to change the checked items
# This is standard behavior (on macos) but I think it is surprising when it has a multiple select
if self.justShown:
@ -448,7 +446,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
res = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.CheckState.Checked:
if item.checkState() == Qt.Checked:
res.append(self.itemData(i))
return res
@ -460,7 +458,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
self.model().item(0).setCheckState(Qt.CheckState.Checked)
# Add room for "move" arrows
text_width = self.fontMetrics().boundingRect(text).width()
text_width = self.fontMetrics().width(text)
checkbox_width = 40
total_width = text_width + checkbox_width + (self.itemDelegate().button_width * 2)
if total_width > self.view().minimumWidth():
@ -479,19 +477,19 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
return
# Grab values for the rows to swap
cur_data = self.model().item(index).data(Qt.ItemDataRole.UserRole)
cur_title = self.model().item(index).data(Qt.ItemDataRole.DisplayRole)
cur_state = self.model().item(index).checkState()
cur_data = self.model().item(index).data(Qt.UserRole)
cur_title = self.model().item(index).data(Qt.DisplayRole)
cur_state = self.model().item(index).data(Qt.CheckStateRole)
swap_data = self.model().item(row).data(Qt.ItemDataRole.UserRole)
swap_title = self.model().item(row).data(Qt.ItemDataRole.DisplayRole)
swap_data = self.model().item(row).data(Qt.UserRole)
swap_title = self.model().item(row).data(Qt.DisplayRole)
swap_state = self.model().item(row).checkState()
self.model().item(row).setData(cur_data, Qt.ItemDataRole.UserRole)
self.model().item(row).setData(cur_data, Qt.UserRole)
self.model().item(row).setCheckState(cur_state)
self.model().item(row).setText(cur_title)
self.model().item(index).setData(swap_data, Qt.ItemDataRole.UserRole)
self.model().item(index).setData(swap_data, Qt.UserRole)
self.model().item(index).setCheckState(swap_state)
self.model().item(index).setText(swap_title)
@ -499,7 +497,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
texts = []
for i in range(self.count()):
item = self.model().item(i)
if item.checkState() == Qt.CheckState.Checked:
if item.checkState() == Qt.Checked:
texts.append(item.text())
text = ", ".join(texts)
@ -511,24 +509,22 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
so.initFrom(self)
# Ask the style for the size of the text field
rect = self.style().subControlRect(
QtWidgets.QStyle.ComplexControl.CC_ComboBox, so, QtWidgets.QStyle.SubControl.SC_ComboBoxEditField
)
rect = self.style().subControlRect(QtWidgets.QStyle.CC_ComboBox, so, QtWidgets.QStyle.SC_ComboBoxEditField)
# Compute the elided text
elidedText = self.fontMetrics().elidedText(text, Qt.TextElideMode.ElideRight, rect.width())
elidedText = self.fontMetrics().elidedText(text, Qt.ElideRight, rect.width())
# This CheckableComboBox does not use the index, so we clear it and set the placeholder text
self.setCurrentIndex(-1)
self.setPlaceholderText(elidedText)
def setItemChecked(self, index: Any, state: bool) -> None:
qt_state = Qt.CheckState.Checked if state else Qt.CheckState.Unchecked
qt_state = Qt.Checked if state else Qt.Unchecked
item = self.model().item(index)
current = self.currentData()
# If we have at least one item checked emit itemChecked with the current check state and update text
# Require at least one item to be checked and provide a tooltip
if len(current) == 1 and not state and item.checkState() == Qt.CheckState.Checked:
if len(current) == 1 and not state and item.checkState() == Qt.Checked:
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), self.toolTip(), self, QRect(), 3000)
return
@ -537,7 +533,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
self._updateText()
def toggleItem(self, index: int) -> None:
if self.model().item(index).checkState() == Qt.CheckState.Checked:
if self.model().item(index).checkState() == Qt.Checked:
self.setItemChecked(index, False)
else:
self.setItemChecked(index, True)

@ -8,14 +8,14 @@ import traceback
import webbrowser
from collections.abc import Collection, Sequence
from PyQt6.QtCore import QUrl
from PyQt6.QtWidgets import QWidget
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QWidget
logger = logging.getLogger(__name__)
try:
from PyQt6 import QtGui, QtWidgets
from PyQt6.QtCore import Qt
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import Qt
qt_available = True
except ImportError:
@ -30,8 +30,7 @@ if qt_available:
pil_available = False
try:
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
class WebPage(QWebEnginePage):
def acceptNavigationRequest(
@ -54,7 +53,6 @@ if qt_available:
webengine.setPage(WebPage(parent))
webengine.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
settings = webengine.settings()
assert settings is not None
settings.setAttribute(settings.WebAttribute.AutoLoadImages, True)
settings.setAttribute(settings.WebAttribute.JavascriptEnabled, False)
settings.setAttribute(settings.WebAttribute.JavascriptCanOpenWindows, False)
@ -192,7 +190,7 @@ if qt_available:
if isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
widget.setReadOnly(False)
elif isinstance(widget, QtWidgets.QListWidget):
widget.setMovement(QtWidgets.QListWidget.Movement.Free)
widget.setMovement(QtWidgets.QListWidget.Free)
else:
if isinstance(widget, QtWidgets.QTableWidgetItem):
widget.setBackground(inactive_brush)
@ -209,7 +207,7 @@ if qt_available:
elif isinstance(widget, QtWidgets.QListWidget):
inactive_palette = palettes()
widget.setPalette(inactive_palette[0])
widget.setMovement(QtWidgets.QListWidget.Movement.Static)
widget.setMovement(QtWidgets.QListWidget.Static)
def replaceWidget(
layout: QtWidgets.QLayout | QtWidgets.QSplitter, old_widget: QtWidgets.QWidget, new_widget: QtWidgets.QWidget

@ -6,7 +6,7 @@ from pathlib import Path
from typing import Any, NamedTuple, cast
import settngs
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt5 import QtCore, QtGui, QtWidgets
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ctsettings import ct_ns, group_for_plugin
@ -39,13 +39,11 @@ class PasswordEdit(QtWidgets.QLineEdit):
self.visibleIcon = QtGui.QIcon(":/graphics/eye.svg")
self.hiddenIcon = QtGui.QIcon(":/graphics/hidden.svg")
self.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.setEchoMode(QtWidgets.QLineEdit.Password)
if show_visibility:
# Add the password hide/shown toggle at the end of the edit box.
self.togglepasswordAction = self.addAction(
self.visibleIcon, QtWidgets.QLineEdit.ActionPosition.TrailingPosition
)
self.togglepasswordAction = self.addAction(self.visibleIcon, QtWidgets.QLineEdit.TrailingPosition)
self.togglepasswordAction.setToolTip("Show password")
self.togglepasswordAction.triggered.connect(self.on_toggle_password_action)
@ -53,12 +51,12 @@ class PasswordEdit(QtWidgets.QLineEdit):
def on_toggle_password_action(self) -> None:
if not self.password_shown:
self.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal)
self.setEchoMode(QtWidgets.QLineEdit.Normal)
self.password_shown = True
self.togglepasswordAction.setIcon(self.hiddenIcon)
self.togglepasswordAction.setToolTip("Hide password")
else:
self.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.setEchoMode(QtWidgets.QLineEdit.Password)
self.password_shown = False
self.togglepasswordAction.setIcon(self.visibleIcon)
self.togglepasswordAction.setToolTip("Show password")
@ -127,7 +125,7 @@ def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) ->
widget = QtWidgets.QSpinBox()
widget.setRange(0, 9999)
widget.setToolTip(option.help)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignmentFlag.AlignLeft)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft)
return widget
@ -140,7 +138,7 @@ def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayou
widget = QtWidgets.QDoubleSpinBox()
widget.setRange(0, 9999.99)
widget.setToolTip(option.help)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignmentFlag.AlignLeft)
layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft)
return widget
@ -225,8 +223,8 @@ def generate_talker_info(talker: ComicTalker, config: settngs.Config[ct_ns], lay
# Add horizontal divider
line = QtWidgets.QFrame()
line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(line, row + 3, 0, 1, -1)
@ -354,15 +352,15 @@ def generate_source_option_tabs(
talker_layout = QtWidgets.QGridLayout()
lbl_select_talker = QtWidgets.QLabel("Metadata Source:")
line = QtWidgets.QFrame()
line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
talker_tabs = QtWidgets.QTabWidget()
# Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget
sources: Sources = Sources(QtWidgets.QComboBox(), [])
talker_layout.addWidget(lbl_select_talker, 0, 0)
talker_layout.addWidget(sources[0], 0, 1)
talker_layout.addWidget(lbl_select_talker, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
talker_layout.addWidget(sources[0], 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
talker_layout.addWidget(line, 1, 0, 1, -1)
talker_layout.addWidget(talker_tabs, 2, 0, 1, -1)
@ -442,9 +440,7 @@ def generate_source_option_tabs(
generate_api_widgets(talker, tab, key_option, url_option, layout_grid, definitions=config.definitions)
# Add vertical spacer
vspacer = QtWidgets.QSpacerItem(
20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding
)
vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0)
# Display the new widgets
tab.tab.setLayout(layout_grid)

@ -43,7 +43,7 @@ def cleanup_html(string: str | None, remove_html_tables: bool = False) -> str:
# find any tables
soup = BeautifulSoup(string, "html.parser")
tables = soup.find_all("table")
tables = soup.findAll("table")
# put in our own
string = re.sub(r"<br>|</li>", "\n", string, flags=re.IGNORECASE)
@ -78,15 +78,15 @@ def cleanup_html(string: str | None, remove_html_tables: bool = False) -> str:
rows = []
hdrs = []
col_widths = []
for hdr in table.find_all("th"):
for hdr in table.findAll("th"):
item = hdr.string.strip()
hdrs.append(item)
col_widths.append(len(item))
rows.append(hdrs)
for row in table.find_all("tr"):
for row in table.findAll("tr"):
cols = []
col = row.find_all("td")
col = row.findAll("td")
for i, c in enumerate(col):
item = c.string.strip()

@ -23,4 +23,4 @@ disable = "C0330, C0326, C0115, C0116, C0103"
max-line-length=120
[tool.pylint.master]
extension-pkg-whitelist="PyQt6"
extension-pkg-whitelist="PyQt5"

@ -77,8 +77,8 @@ pyinstaller40 =
7z =
py7zr
all =
PyQt6
PyQt6-WebEngine
PyQt5
PyQtWebEngine
comicinfoxml==0.4.*
gcd-talker>0.1.0
metron-talker>0.1.5
@ -98,7 +98,7 @@ cix =
gcd =
gcd-talker>0.1.0
gui =
PyQt6
PyQt5
icu =
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
jxl =
@ -106,8 +106,8 @@ jxl =
metron =
metron-talker>0.1.5
pyinstaller =
PyQt6
PyQt6-WebEngine
PyQt5
PyQtWebEngine
comicinfoxml==0.4.*
pillow-avif-plugin>=1.4.1
pillow-jxl-plugin>=1.2.5
@ -115,8 +115,8 @@ pyinstaller =
rarfile>=4.0
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
qtw =
PyQt6
PyQt6-WebEngine
PyQt5
PyQtWebEngine
[options.package_data]
comicapi =
@ -257,6 +257,7 @@ deps =
extras =
pyinstaller
commands =
pyrcc5 comictaggerlib/graphics/graphics.qrc -o comictaggerlib/graphics/resources.py
pyinstaller -y build-tools/comictagger.spec
python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu
@ -274,11 +275,11 @@ depends =
deps =
requests
allowlist_externals =
{tox_root}/build/appimagetool-aarch64.AppImage
{tox_root}/build/appimagetool-x86_64.AppImage
change_dir = {tox_root}/dist/binary
commands_pre =
-python -c 'import shutil; shutil.rmtree("{tox_root}/build/", ignore_errors=True)'
python {tox_root}/build-tools/get_appimage.py {tox_root}/build/appimagetool.AppImage
python {tox_root}/build-tools/get_appimage.py {tox_root}/build/appimagetool-x86_64.AppImage
commands =
python -c 'import shutil,pathlib; shutil.copytree("{tox_root}/dist/comictagger/", "{tox_root}/build/appimage", dirs_exist_ok=True); \
shutil.copy("{tox_root}/comictaggerlib/graphics/app.png", "{tox_root}/build/appimage/app.png"); \
@ -286,7 +287,7 @@ commands =
pathlib.Path("{tox_root}/build/appimage/AppRun.desktop").write_text( \
pathlib.Path("{tox_root}/build-tools/ComicTagger.desktop").read_text() \
.replace("/usr/local/share/comictagger/app.png", "app"))'
{tox_root}/build/appimagetool.AppImage {tox_root}/build/appimage
{tox_root}/build/appimagetool-x86_64.AppImage {tox_root}/build/appimage
[testenv:zip_artifacts]
description = Zip release artifacts

@ -258,7 +258,7 @@ all_seed_imprints = {
"Marvel": seed_imprints["Marvel"].copy(),
"DC Comics": additional_seed_imprints["DC Comics"].copy(),
}
all_seed_imprints["Marvel"].update(additional_seed_imprints["Marvel"].items())
all_seed_imprints["Marvel"].update(additional_seed_imprints["Marvel"])
conflicting_seed_imprints = {"Marvel": {"test": "Never"}}