diff --git a/naturtag/client.py b/naturtag/client.py index 17c00939..1f82d0dd 100644 --- a/naturtag/client.py +++ b/naturtag/client.py @@ -268,8 +268,3 @@ def get_url_hash(url: str) -> str: thumbnail_hash = md5(url.encode()).hexdigest() ext = Photo(url=url).ext return f'{thumbnail_hash}.{ext}' - - -# TODO: Refactor to depend on app.client and session objects instead of these module-level globals -INAT_CLIENT = iNatDbClient() -IMG_SESSION = ImageSession() diff --git a/naturtag/controllers/observation_view.py b/naturtag/controllers/observation_view.py index 06b1c92a..6bc99f01 100644 --- a/naturtag/controllers/observation_view.py +++ b/naturtag/controllers/observation_view.py @@ -18,6 +18,7 @@ ObservationImageWindow, ObservationPhoto, VerticalLayout, + set_pixmap_async, ) from naturtag.widgets.observation_images import GEOPRIVACY_ICONS, QUALITY_GRADE_ICONS @@ -119,8 +120,9 @@ def load(self, obs: Observation): self.group_box.setTitle(obs.taxon.full_name) self.image.hover_event = True self.image.observation = obs - self.image.set_pixmap_async( - photo=obs.photos[0], # TODO: add Observation.default_photo in pyinat + set_pixmap_async( + self.image, + photo=Observation.default_photo, priority=QThread.HighPriority, ) self._update_nav_buttons() @@ -131,7 +133,7 @@ def load(self, obs: Observation): thumb = ObservationPhoto(observation=obs, idx=i + 1, rounded=True) thumb.setFixedSize(*SIZE_SM) thumb.on_click.connect(self.image_window.display_observation_fullscreen) - thumb.set_pixmap_async(photo=photo, size='thumbnail') + set_pixmap_async(thumb, photo=photo, size='thumbnail') self.thumbnails.addWidget(thumb) # Load observation details diff --git a/naturtag/controllers/taxon_view.py b/naturtag/controllers/taxon_view.py index b16bcf1a..668ef40d 100644 --- a/naturtag/controllers/taxon_view.py +++ b/naturtag/controllers/taxon_view.py @@ -10,7 +10,6 @@ from naturtag.app.style import fa_icon from naturtag.constants import SIZE_SM -from naturtag.controllers import get_app from naturtag.settings import UserTaxa from naturtag.widgets import ( GridLayout, @@ -18,6 +17,7 @@ TaxonImageWindow, TaxonInfoCard, TaxonList, + set_pixmap_async, ) from naturtag.widgets.layouts import VerticalLayout from naturtag.widgets.taxon_images import TaxonPhoto @@ -110,7 +110,7 @@ def load(self, taxon: Taxon): self.group_box.setTitle(taxon.full_name) self.image.hover_event = True self.image.taxon = taxon - self.image.set_pixmap_async(photo=taxon.default_photo, priority=QThread.HighPriority) + set_pixmap_async(self.image, photo=taxon.default_photo, priority=QThread.HighPriority) self._update_nav_buttons() # Load additional thumbnails @@ -119,7 +119,7 @@ def load(self, taxon: Taxon): thumb = TaxonPhoto(taxon=taxon, idx=i + 1, rounded=True) thumb.setFixedSize(*SIZE_SM) thumb.on_click.connect(self.image_window.display_taxon_fullscreen) - thumb.set_pixmap_async(photo=photo, size='thumbnail') + set_pixmap_async(thumb, photo=photo, size='thumbnail') self.thumbnails.addWidget(thumb) def prev(self): diff --git a/naturtag/metadata/inat_metadata.py b/naturtag/metadata/inat_metadata.py index 29dfca8b..a26e0b04 100644 --- a/naturtag/metadata/inat_metadata.py +++ b/naturtag/metadata/inat_metadata.py @@ -9,7 +9,7 @@ from pyinaturalist import Observation, Taxon from pyinaturalist_convert import to_dwc -from naturtag.client import INAT_CLIENT, iNatDbClient +from naturtag.client import iNatDbClient from naturtag.constants import COMMON_NAME_IGNORE_TERMS, COMMON_RANKS, PathOrStr from naturtag.metadata import MetaMetadata from naturtag.settings import Settings @@ -55,8 +55,8 @@ def tag_images( Returns: Updated image metadata for each image """ - client = client or INAT_CLIENT settings = settings or Settings.read() + client = client or iNatDbClient(settings.db_path) observation = client.from_ids(observation_id, taxon_id) if not observation: @@ -115,8 +115,8 @@ def refresh_tags( Returns: Updated metadata objects for updated images only """ - client = client or INAT_CLIENT settings = settings or Settings.read() + client = client or iNatDbClient(settings.db_path) metadata_objs = [ _refresh_tags(MetaMetadata(image_path), client, settings) for image_path in get_valid_image_paths( diff --git a/naturtag/widgets/__init__.py b/naturtag/widgets/__init__.py index 670c308f..39ab827d 100644 --- a/naturtag/widgets/__init__.py +++ b/naturtag/widgets/__init__.py @@ -19,6 +19,8 @@ InfoCardList, ImageWindow, PixmapLabel, + set_pixmap, + set_pixmap_async, ) from naturtag.widgets.inputs import IdInput from naturtag.widgets.logger import QtRichHandler, init_handler diff --git a/naturtag/widgets/images.py b/naturtag/widgets/images.py index f892a6ac..d981b73d 100644 --- a/naturtag/widgets/images.py +++ b/naturtag/widgets/images.py @@ -5,13 +5,11 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterator, Optional, TypeAlias, Union -from pyinaturalist import Photo from PySide6.QtCore import QSize, Qt, QThread, Signal from PySide6.QtGui import QBrush, QFont, QIcon, QPainter, QPixmap from PySide6.QtWidgets import QLabel, QLayout, QScrollArea, QSizePolicy, QWidget from naturtag.app.style import fa_icon -from naturtag.client import IMG_SESSION from naturtag.constants import SIZE_ICON, SIZE_ICON_SM, SIZE_SM, IconDimensions, IntOrStr, PathOrStr from naturtag.widgets import StylableWidget, VerticalLayout from naturtag.widgets.layouts import GridLayout, HorizontalLayout @@ -24,6 +22,28 @@ logger = getLogger(__name__) +def set_pixmap_async( + pixmap_label: QLabel, + priority: QThread.Priority = QThread.NormalPriority, + **kwargs, +): + """Fetch an image from a separate thread, and render it in the main thread when complete""" + from naturtag.controllers import get_app + + app = get_app() + future = app.threadpool.schedule(app.img_session.get_pixmap, priority=priority, **kwargs) + future.on_result.connect(pixmap_label.setPixmap) + + +def set_pixmap(pixmap_label: QLabel, *args, **kwargs): + """Fetch an image from either a local path or remote URL.""" + from naturtag.controllers import get_app + + app = get_app() + pixmap = app.img_session.get_pixmap(*args, **kwargs) + pixmap_label.setPixmap(pixmap) + + class FAIcon(QLabel): """A QLabel for displaying a FontAwesome icon""" @@ -129,43 +149,14 @@ def __init__( self.rounded = rounded self.scale = scale self.xform = Qt.SmoothTransformation if resample else Qt.FastTransformation - if path or url: - pixmap = self.get_pixmap(path=path, url=url) self.setPixmap(pixmap) - - # TODO: Need quite a bit of refactoring to not depend on global session object - # Pass app.img_session as a parameter instead - def get_pixmap(self, *args, **kwargs) -> QPixmap: - """Fetch a pixmap from either a local path or remote URL. - This does not render the image, so it is safe to run from any thread. - """ - return IMG_SESSION.get_pixmap(*args, **kwargs) + if path or url: + set_pixmap(self, path=path, url=url) def setPixmap(self, pixmap: QPixmap): self._pixmap = pixmap super().setPixmap(self.scaledPixmap()) - def set_pixmap_async( - self, - priority: QThread.Priority = QThread.NormalPriority, - path: Optional[PathOrStr] = None, - photo: Optional[Photo] = None, - size: str = 'medium', - url: Optional[str] = None, - ): - """Fetch a photo from a separate thread, and render it in the main thread when complete""" - from naturtag.controllers import get_app - - future = get_app().threadpool.schedule( - self.get_pixmap, - priority=priority, - path=path, - photo=photo, - url=url, - size=size, - ) - future.on_result.connect(self.setPixmap) - def clear(self): self.setPixmap(QPixmap()) @@ -392,7 +383,7 @@ def add_card(self, card: InfoCard, thumbnail_url: str, idx: Optional[int] = None self.root.insertWidget(idx, card) else: self.root.addWidget(card) - card.thumbnail.set_pixmap_async(url=thumbnail_url) + set_pixmap_async(card.thumbnail, url=thumbnail_url) def clear(self): self.root.clear() diff --git a/naturtag/widgets/observation_images.py b/naturtag/widgets/observation_images.py index cfafaebb..980253ed 100644 --- a/naturtag/widgets/observation_images.py +++ b/naturtag/widgets/observation_images.py @@ -7,7 +7,14 @@ from PySide6.QtCore import Qt from naturtag.constants import SIZE_ICON_SM -from naturtag.widgets.images import HoverPhoto, IconLabel, ImageWindow, InfoCard, InfoCardList +from naturtag.widgets.images import ( + HoverPhoto, + IconLabel, + ImageWindow, + InfoCard, + InfoCardList, + set_pixmap, +) from naturtag.widgets.layouts import HorizontalLayout logger = getLogger(__name__) @@ -41,8 +48,7 @@ def __init__(self, obs: Observation, delayed_load: bool = True): self.observation = obs if not delayed_load: - pixmap = self.thumbnail.get_pixmap(url=obs.default_photo.thumbnail_url) - self.thumbnail.setPixmap(pixmap) + set_pixmap(self.thumbnail, url=obs.default_photo.thumbnail_url) # Title: Taxon name if obs.taxon: @@ -173,7 +179,7 @@ def select_image_idx(self, idx: int): self.set_photo(self.selected_photo) def set_photo(self, photo: Photo): - self.image.setPixmap(self.image.get_pixmap(url=photo.original_url)) + set_pixmap(self.image, url=photo.original_url) def remove_image(self): pass diff --git a/naturtag/widgets/taxon_images.py b/naturtag/widgets/taxon_images.py index f54a2610..e5dc8624 100644 --- a/naturtag/widgets/taxon_images.py +++ b/naturtag/widgets/taxon_images.py @@ -16,6 +16,7 @@ ImageWindow, InfoCard, InfoCardList, + set_pixmap, ) if TYPE_CHECKING: @@ -42,8 +43,7 @@ def __init__(self, taxon: Taxon, user_observations_count: int = 0, delayed_load: self.setFixedHeight(90) self.taxon = taxon if not delayed_load: - pixmap = self.thumbnail.get_pixmap(url=taxon.default_photo.thumbnail_url) - self.thumbnail.setPixmap(pixmap) + set_pixmap(self.thumbnail, url=taxon.default_photo.thumbnail_url) # Details self.title.setText(f'{taxon.rank.title()}: {taxon.name}') @@ -128,7 +128,7 @@ def select_image_idx(self, idx: int): self.set_photo(self.selected_photo) def set_photo(self, photo: Photo): - self.image.setPixmap(self.image.get_pixmap(url=photo.original_url)) + set_pixmap(self.image, url=photo.original_url) attribution = ( ATTRIBUTION_STRIP_PATTERN.sub('', photo.attribution or '') .replace('(c)', '©')