b9af606f87
Cover image scaling now uses the smooth transformation option in Qt Filename parsing now identifies a single number as a filename e.g. '52.cbz' gets parsed as issue: 52 and series: 52
329 lines
11 KiB
Python
329 lines
11 KiB
Python
"""A PyQt5 widget to display cover images
|
|
|
|
Display cover images from either a local archive, or from Comic Vine.
|
|
TODO: This should be re-factored using subclasses!
|
|
"""
|
|
#
|
|
# Copyright 2012-2014 Anthony Beville
|
|
#
|
|
# 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 logging
|
|
from typing import Callable, cast
|
|
|
|
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
|
|
|
from comicapi import utils
|
|
from comicapi.comicarchive import ComicArchive
|
|
from comictaggerlib.comicvinetalker import ComicVineTalker
|
|
from comictaggerlib.imagefetcher import ImageFetcher
|
|
from comictaggerlib.imagepopup import ImagePopup
|
|
from comictaggerlib.pageloader import PageLoader
|
|
from comictaggerlib.settings import ComicTaggerSettings
|
|
from comictaggerlib.ui.qtutils import get_qimage_from_data, reduce_widget_font_size
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def clickable(widget: QtWidgets.QWidget) -> QtCore.pyqtBoundSignal:
|
|
"""Allow a label to be clickable"""
|
|
|
|
class Filter(QtCore.QObject):
|
|
|
|
dblclicked = QtCore.pyqtSignal()
|
|
|
|
def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool:
|
|
if obj == widget:
|
|
if event.type() == QtCore.QEvent.Type.MouseButtonDblClick:
|
|
self.dblclicked.emit()
|
|
return True
|
|
return False
|
|
|
|
flt = Filter(widget)
|
|
widget.installEventFilter(flt)
|
|
return flt.dblclicked
|
|
|
|
|
|
class Signal(QtCore.QObject):
|
|
alt_url_list_fetch_complete = QtCore.pyqtSignal(list)
|
|
url_fetch_complete = QtCore.pyqtSignal(str, str)
|
|
image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray)
|
|
|
|
def __init__(
|
|
self,
|
|
list_fetch: Callable[[list[str]], None],
|
|
url_fetch: Callable[[str, str], None],
|
|
image_fetch: Callable[[bytes], None],
|
|
) -> None:
|
|
super().__init__()
|
|
self.alt_url_list_fetch_complete.connect(list_fetch)
|
|
self.url_fetch_complete.connect(url_fetch)
|
|
self.image_fetch_complete.connect(image_fetch)
|
|
|
|
def emit_list(self, url_list: list[str]) -> None:
|
|
self.alt_url_list_fetch_complete.emit(url_list)
|
|
|
|
def emit_url(self, image_url: str, thumb_url: str | None) -> None:
|
|
self.url_fetch_complete.emit(image_url, thumb_url)
|
|
|
|
def emit_image(self, image_data: bytes | QtCore.QByteArray) -> None:
|
|
self.image_fetch_complete.emit(image_data)
|
|
|
|
|
|
class CoverImageWidget(QtWidgets.QWidget):
|
|
ArchiveMode = 0
|
|
AltCoverMode = 1
|
|
URLMode = 1
|
|
DataMode = 3
|
|
|
|
def __init__(self, parent: QtWidgets.QWidget, mode: int, expand_on_click: bool = True) -> None:
|
|
super().__init__(parent)
|
|
|
|
uic.loadUi(ComicTaggerSettings.get_ui_file("coverimagewidget.ui"), self)
|
|
|
|
reduce_widget_font_size(self.label)
|
|
|
|
self.sig = Signal(
|
|
self.alt_cover_url_list_fetch_complete, self.primary_url_fetch_complete, self.cover_remote_fetch_complete
|
|
)
|
|
|
|
self.mode: int = mode
|
|
self.page_loader: PageLoader | None = None
|
|
self.showControls = True
|
|
|
|
self.current_pixmap = QtGui.QPixmap()
|
|
|
|
self.comic_archive: ComicArchive | None = None
|
|
self.issue_id: int | None = None
|
|
self.url_list: list[str] = []
|
|
if self.page_loader is not None:
|
|
self.page_loader.abandoned = True
|
|
self.page_loader = None
|
|
self.imageIndex = -1
|
|
self.imageCount = 1
|
|
self.imageData = bytes()
|
|
|
|
self.btnLeft.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("left.png")))
|
|
self.btnRight.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("right.png")))
|
|
|
|
self.btnLeft.clicked.connect(self.decrement_image)
|
|
self.btnRight.clicked.connect(self.increment_image)
|
|
if expand_on_click:
|
|
clickable(self.lblImage).connect(self.show_popup)
|
|
else:
|
|
self.lblImage.setToolTip("")
|
|
|
|
self.update_content()
|
|
|
|
def reset_widget(self) -> None:
|
|
self.comic_archive = None
|
|
self.issue_id = None
|
|
self.url_list = []
|
|
if self.page_loader is not None:
|
|
self.page_loader.abandoned = True
|
|
self.page_loader = None
|
|
self.imageIndex = -1
|
|
self.imageCount = 1
|
|
self.imageData = bytes()
|
|
|
|
def clear(self) -> None:
|
|
self.reset_widget()
|
|
self.update_content()
|
|
|
|
def increment_image(self) -> None:
|
|
self.imageIndex += 1
|
|
if self.imageIndex == self.imageCount:
|
|
self.imageIndex = 0
|
|
self.update_content()
|
|
|
|
def decrement_image(self) -> None:
|
|
self.imageIndex -= 1
|
|
if self.imageIndex == -1:
|
|
self.imageIndex = self.imageCount - 1
|
|
self.update_content()
|
|
|
|
def set_archive(self, ca: ComicArchive, page: int = 0) -> None:
|
|
if self.mode == CoverImageWidget.ArchiveMode:
|
|
self.reset_widget()
|
|
self.comic_archive = ca
|
|
self.imageIndex = page
|
|
self.imageCount = ca.get_number_of_pages()
|
|
self.update_content()
|
|
|
|
def set_url(self, url: str) -> None:
|
|
if self.mode == CoverImageWidget.URLMode:
|
|
self.reset_widget()
|
|
self.update_content()
|
|
|
|
self.url_list = [url]
|
|
self.imageIndex = 0
|
|
self.imageCount = 1
|
|
self.update_content()
|
|
|
|
def set_issue_id(self, issue_id: int) -> None:
|
|
if self.mode == CoverImageWidget.AltCoverMode:
|
|
self.reset_widget()
|
|
self.update_content()
|
|
self.issue_id = issue_id
|
|
|
|
comic_vine = ComicVineTalker()
|
|
ComicVineTalker.url_fetch_complete = self.sig.emit_url
|
|
comic_vine.async_fetch_issue_cover_urls(self.issue_id)
|
|
|
|
def set_image_data(self, image_data: bytes) -> None:
|
|
if self.mode == CoverImageWidget.DataMode:
|
|
self.reset_widget()
|
|
|
|
if image_data:
|
|
self.imageIndex = 0
|
|
self.imageData = image_data
|
|
else:
|
|
self.imageIndex = -1
|
|
|
|
self.update_content()
|
|
|
|
def primary_url_fetch_complete(self, primary_url: str, thumb_url: str | None) -> None:
|
|
self.url_list.append(str(primary_url))
|
|
self.imageIndex = 0
|
|
self.imageCount = len(self.url_list)
|
|
self.update_content()
|
|
|
|
# defer the alt cover search
|
|
QtCore.QTimer.singleShot(1, self.start_alt_cover_search)
|
|
|
|
def start_alt_cover_search(self) -> None:
|
|
|
|
if self.issue_id is not None:
|
|
# now we need to get the list of alt cover URLs
|
|
self.label.setText("Searching for alt. covers...")
|
|
|
|
# page URL should already be cached, so no need to defer
|
|
comic_vine = ComicVineTalker()
|
|
issue_page_url = comic_vine.fetch_issue_page_url(self.issue_id)
|
|
ComicVineTalker.alt_url_list_fetch_complete = self.sig.emit_list
|
|
comic_vine.async_fetch_alternate_cover_urls(utils.xlate(self.issue_id), cast(str, issue_page_url))
|
|
|
|
def alt_cover_url_list_fetch_complete(self, url_list: list[str]) -> None:
|
|
if len(url_list) > 0:
|
|
self.url_list.extend(url_list)
|
|
self.imageCount = len(self.url_list)
|
|
self.update_controls()
|
|
|
|
def set_page(self, pagenum: int) -> None:
|
|
if self.mode == CoverImageWidget.ArchiveMode:
|
|
self.imageIndex = pagenum
|
|
self.update_content()
|
|
|
|
def update_content(self) -> None:
|
|
self.update_image()
|
|
self.update_controls()
|
|
|
|
def update_image(self) -> None:
|
|
if self.imageIndex == -1:
|
|
self.load_default()
|
|
elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]:
|
|
self.load_url()
|
|
elif self.mode == CoverImageWidget.DataMode:
|
|
self.cover_remote_fetch_complete(self.imageData)
|
|
else:
|
|
self.load_page()
|
|
|
|
def update_controls(self) -> None:
|
|
if not self.showControls or self.mode == CoverImageWidget.DataMode:
|
|
self.btnLeft.hide()
|
|
self.btnRight.hide()
|
|
self.label.hide()
|
|
return
|
|
|
|
if self.imageIndex == -1 or self.imageCount == 1:
|
|
self.btnLeft.setEnabled(False)
|
|
self.btnRight.setEnabled(False)
|
|
self.btnLeft.hide()
|
|
self.btnRight.hide()
|
|
else:
|
|
self.btnLeft.setEnabled(True)
|
|
self.btnRight.setEnabled(True)
|
|
self.btnLeft.show()
|
|
self.btnRight.show()
|
|
|
|
if self.imageIndex == -1 or self.imageCount == 1:
|
|
self.label.setText("")
|
|
elif self.mode == CoverImageWidget.AltCoverMode:
|
|
self.label.setText(f"Cover {self.imageIndex + 1} (of {self.imageCount})")
|
|
else:
|
|
self.label.setText(f"Page {self.imageIndex + 1} (of {self.imageCount})")
|
|
|
|
def load_url(self) -> None:
|
|
self.load_default()
|
|
self.cover_fetcher = ImageFetcher()
|
|
ImageFetcher.image_fetch_complete = self.sig.emit_image
|
|
self.cover_fetcher.fetch(self.url_list[self.imageIndex])
|
|
|
|
# called when the image is done loading from internet
|
|
def cover_remote_fetch_complete(self, image_data: bytes) -> None:
|
|
img = get_qimage_from_data(image_data)
|
|
self.current_pixmap = QtGui.QPixmap.fromImage(img)
|
|
self.set_display_pixmap()
|
|
|
|
def load_page(self) -> None:
|
|
if self.comic_archive is not None:
|
|
if self.page_loader is not None:
|
|
self.page_loader.abandoned = True
|
|
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
|
|
self.page_loader.loadComplete.connect(self.page_load_complete)
|
|
self.page_loader.start()
|
|
|
|
def page_load_complete(self, image_data: bytes) -> None:
|
|
img = get_qimage_from_data(image_data)
|
|
self.current_pixmap = QtGui.QPixmap.fromImage(img)
|
|
self.set_display_pixmap()
|
|
self.page_loader = None
|
|
|
|
def load_default(self) -> None:
|
|
self.current_pixmap = QtGui.QPixmap(ComicTaggerSettings.get_graphic("nocover.png"))
|
|
self.set_display_pixmap()
|
|
|
|
def resizeEvent(self, resize_event: QtGui.QResizeEvent) -> None:
|
|
if self.current_pixmap is not None:
|
|
self.set_display_pixmap()
|
|
|
|
def set_display_pixmap(self) -> None:
|
|
"""The deltas let us know what the new width and height of the label will be"""
|
|
|
|
new_h = self.frame.height()
|
|
new_w = self.frame.width()
|
|
frame_w = self.frame.width()
|
|
frame_h = self.frame.height()
|
|
|
|
new_h -= 4
|
|
new_w -= 4
|
|
|
|
new_h = max(new_h, 0)
|
|
new_w = max(new_w, 0)
|
|
|
|
# scale the pixmap to fit in the frame
|
|
scaled_pixmap = self.current_pixmap.scaled(
|
|
new_w, new_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.SmoothTransformation
|
|
)
|
|
self.lblImage.setPixmap(scaled_pixmap)
|
|
|
|
# move and resize the label to be centered in the fame
|
|
img_w = scaled_pixmap.width()
|
|
img_h = scaled_pixmap.height()
|
|
self.lblImage.resize(img_w, img_h)
|
|
self.lblImage.move(int((frame_w - img_w) / 2), int((frame_h - img_h) / 2))
|
|
|
|
def show_popup(self) -> None:
|
|
ImagePopup(self, self.current_pixmap)
|