mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-31 15:19:10 +00:00
fix: restore page navigation state (#933)
* refactor: store browsing history for navigation purposes * refactor: remove page_size from FilterState * refactor: move on from the term "filter" in favor of "BrowsingState" * fix: refactors didn't propagate to the tests * fix: ruff complaints * fix: remaing refactoring errors * fix: navigation works again * fix: also store and restore query
This commit is contained in:
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.query_lang.ast import AST, Constraint, ConstraintType
|
||||
from tagstudio.core.query_lang.ast import AST
|
||||
from tagstudio.core.query_lang.parser import Parser
|
||||
|
||||
MAX_SQL_VARIABLES = 32766 # 32766 is the max sql bind parameter count as defined here: https://github.com/sqlite/sqlite/blob/master/src/sqliteLimit.h#L140
|
||||
@@ -72,59 +72,57 @@ class SortingModeEnum(enum.Enum):
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilterState:
|
||||
class BrowsingState:
|
||||
"""Represent a state of the Library grid view."""
|
||||
|
||||
# these should remain
|
||||
page_size: int
|
||||
page_index: int = 0
|
||||
sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED
|
||||
ascending: bool = True
|
||||
|
||||
# these should be erased on update
|
||||
query: str | None = None
|
||||
|
||||
# Abstract Syntax Tree Of the current Search Query
|
||||
ast: AST | None = None
|
||||
|
||||
@property
|
||||
def limit(self):
|
||||
return self.page_size
|
||||
|
||||
@property
|
||||
def offset(self):
|
||||
return self.page_size * self.page_index
|
||||
def ast(self) -> AST | None:
|
||||
if self.query is None:
|
||||
return None
|
||||
return Parser(self.query).parse()
|
||||
|
||||
@classmethod
|
||||
def show_all(cls, page_size: int) -> "FilterState":
|
||||
return FilterState(page_size=page_size)
|
||||
def show_all(cls) -> "BrowsingState":
|
||||
return BrowsingState()
|
||||
|
||||
@classmethod
|
||||
def from_search_query(cls, search_query: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Parser(search_query).parse(), page_size=page_size)
|
||||
def from_search_query(cls, search_query: str) -> "BrowsingState":
|
||||
return cls(query=search_query)
|
||||
|
||||
@classmethod
|
||||
def from_tag_id(cls, tag_id: int | str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.TagID, str(tag_id), []), page_size=page_size)
|
||||
def from_tag_id(cls, tag_id: int | str) -> "BrowsingState":
|
||||
return cls(query=f"tag_id:{str(tag_id)}")
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path | str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.Path, str(path).strip(), []), page_size=page_size)
|
||||
def from_path(cls, path: Path | str) -> "BrowsingState":
|
||||
return cls(query=f'path:"{str(path).strip()}"')
|
||||
|
||||
@classmethod
|
||||
def from_mediatype(cls, mediatype: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.MediaType, mediatype, []), page_size=page_size)
|
||||
def from_mediatype(cls, mediatype: str) -> "BrowsingState":
|
||||
return cls(query=f"mediatype:{mediatype}")
|
||||
|
||||
@classmethod
|
||||
def from_filetype(cls, filetype: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.FileType, filetype, []), page_size=page_size)
|
||||
def from_filetype(cls, filetype: str) -> "BrowsingState":
|
||||
return cls(query=f"filetype:{filetype}")
|
||||
|
||||
@classmethod
|
||||
def from_tag_name(cls, tag_name: str, page_size: int) -> "FilterState":
|
||||
return cls(ast=Constraint(ConstraintType.Tag, tag_name, []), page_size=page_size)
|
||||
def from_tag_name(cls, tag_name: str) -> "BrowsingState":
|
||||
return cls(query=f'tag:"{tag_name}"')
|
||||
|
||||
def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState":
|
||||
def with_page_index(self, index: int) -> "BrowsingState":
|
||||
return replace(self, page_index=index)
|
||||
|
||||
def with_sorting_mode(self, mode: SortingModeEnum) -> "BrowsingState":
|
||||
return replace(self, sorting_mode=mode)
|
||||
|
||||
def with_sorting_direction(self, ascending: bool) -> "FilterState":
|
||||
def with_sorting_direction(self, ascending: bool) -> "BrowsingState":
|
||||
return replace(self, ascending=ascending)
|
||||
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ from tagstudio.core.library.alchemy import default_color_groups
|
||||
from tagstudio.core.library.alchemy.db import make_tables
|
||||
from tagstudio.core.library.alchemy.enums import (
|
||||
MAX_SQL_VARIABLES,
|
||||
BrowsingState,
|
||||
FieldTypeEnum,
|
||||
FilterState,
|
||||
SortingModeEnum,
|
||||
)
|
||||
from tagstudio.core.library.alchemy.fields import (
|
||||
@@ -857,13 +857,14 @@ class Library:
|
||||
|
||||
def search_library(
|
||||
self,
|
||||
search: FilterState,
|
||||
search: BrowsingState,
|
||||
page_size: int,
|
||||
) -> SearchResult:
|
||||
"""Filter library by search query.
|
||||
|
||||
:return: number of entries matching the query and one page of results.
|
||||
"""
|
||||
assert isinstance(search, FilterState)
|
||||
assert isinstance(search, BrowsingState)
|
||||
assert self.engine
|
||||
|
||||
with Session(self.engine, expire_on_commit=False) as session:
|
||||
@@ -902,7 +903,7 @@ class Library:
|
||||
sort_on = func.lower(Entry.path)
|
||||
|
||||
statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on))
|
||||
statement = statement.limit(search.limit).offset(search.offset)
|
||||
statement = statement.limit(page_size).offset(search.page_index * page_size)
|
||||
|
||||
logger.info(
|
||||
"searching library",
|
||||
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry
|
||||
|
||||
@@ -52,7 +52,7 @@ class DupeRegistry:
|
||||
continue
|
||||
|
||||
results = self.library.search_library(
|
||||
FilterState.from_path(path_relative, page_size=500),
|
||||
BrowsingState.from_path(path_relative), 500
|
||||
)
|
||||
|
||||
if not results:
|
||||
|
||||
@@ -63,7 +63,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.relink_class.done.connect(
|
||||
# refresh the grid
|
||||
lambda: (
|
||||
self.driver.filter_items(),
|
||||
self.driver.update_browsing_state(),
|
||||
self.refresh_missing_files(),
|
||||
)
|
||||
)
|
||||
@@ -78,7 +78,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
lambda: (
|
||||
self.set_missing_count(),
|
||||
# refresh the grid
|
||||
self.driver.filter_items(),
|
||||
self.driver.update_browsing_state(),
|
||||
)
|
||||
)
|
||||
self.delete_button.clicked.connect(self.delete_modal.show)
|
||||
|
||||
@@ -24,7 +24,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from tagstudio.core.constants import RESERVED_TAG_END, RESERVED_TAG_START
|
||||
from tagstudio.core.library.alchemy.enums import FilterState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState, TagColorEnum
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.core.palette import ColorType, get_tag_color
|
||||
@@ -292,9 +292,7 @@ class TagSearchPanel(PanelWidget):
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, tag_id=tag.id: (
|
||||
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
|
||||
self.driver.filter_items(
|
||||
FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size)
|
||||
),
|
||||
self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
|
||||
)
|
||||
)
|
||||
tag_widget.search_for_tag_action.setEnabled(True)
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
import contextlib
|
||||
import ctypes
|
||||
import dataclasses
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
@@ -21,6 +20,7 @@ from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from shutil import which
|
||||
from typing import Generic, TypeVar
|
||||
from warnings import catch_warnings
|
||||
|
||||
import structlog
|
||||
@@ -60,8 +60,8 @@ from tagstudio.core.driver import DriverMixin
|
||||
from tagstudio.core.enums import MacroID, SettingItems, ShowFilepathOption
|
||||
from tagstudio.core.global_settings import DEFAULT_GLOBAL_SETTINGS_PATH, GlobalSettings, Theme
|
||||
from tagstudio.core.library.alchemy.enums import (
|
||||
BrowsingState,
|
||||
FieldTypeEnum,
|
||||
FilterState,
|
||||
ItemType,
|
||||
SortingModeEnum,
|
||||
)
|
||||
@@ -121,6 +121,10 @@ else:
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def clamp(value, lower_bound, upper_bound):
|
||||
return max(lower_bound, min(upper_bound, value))
|
||||
|
||||
|
||||
class Consumer(QThread):
|
||||
MARKER_QUIT = "MARKER_QUIT"
|
||||
|
||||
@@ -139,6 +143,38 @@ class Consumer(QThread):
|
||||
pass
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
# Ex. User visits | A ->[B] |
|
||||
# | A B ->[C]|
|
||||
# | A [B]<- C |
|
||||
# |[A]<- B C | Previous routes still exist
|
||||
# | A ->[D] | Stack is cut from [:A] on new route
|
||||
class History(Generic[T]):
|
||||
__history: list[T]
|
||||
__index: int = 0
|
||||
|
||||
def __init__(self, initial_value: T):
|
||||
self.__history = [initial_value]
|
||||
super().__init__()
|
||||
|
||||
def erase_future(self) -> None:
|
||||
self.__history = self.__history[: self.__index + 1]
|
||||
|
||||
def push(self, value: T) -> None:
|
||||
self.erase_future()
|
||||
self.__history.append(value)
|
||||
self.__index = len(self.__history) - 1
|
||||
|
||||
def move(self, delta: int):
|
||||
self.__index = clamp(self.__index + delta, 0, len(self.__history) - 1)
|
||||
|
||||
@property
|
||||
def current(self) -> T:
|
||||
return self.__history[self.__index]
|
||||
|
||||
|
||||
class QtDriver(DriverMixin, QObject):
|
||||
"""A Qt GUI frontend driver for TagStudio."""
|
||||
|
||||
@@ -154,10 +190,13 @@ class QtDriver(DriverMixin, QObject):
|
||||
about_modal: AboutModal
|
||||
unlinked_modal: FixUnlinkedEntriesModal
|
||||
dupe_modal: FixDupeFilesModal
|
||||
|
||||
applied_theme: Theme
|
||||
|
||||
lib: Library
|
||||
|
||||
browsing_history: History[BrowsingState]
|
||||
|
||||
def __init__(self, args: Namespace):
|
||||
super().__init__()
|
||||
# prevent recursive badges update when multiple items selected
|
||||
@@ -167,7 +206,6 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.args = args
|
||||
self.frame_content: list[int] = [] # List of Entry IDs on the current page
|
||||
self.pages_count = 0
|
||||
self.applied_theme = None
|
||||
|
||||
self.scrollbar_pos = 0
|
||||
self.thumb_size = 128
|
||||
@@ -195,7 +233,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
"[Settings] Global Settings File does not exist creating",
|
||||
path=self.global_settings_path,
|
||||
)
|
||||
self.filter = FilterState.show_all(page_size=self.settings.page_size)
|
||||
self.applied_theme = self.settings.theme
|
||||
|
||||
self.__reset_navigation()
|
||||
|
||||
if self.args.cache_file:
|
||||
path = Path(self.args.cache_file)
|
||||
@@ -237,6 +277,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
self.add_tag_to_selected_action: QAction | None = None
|
||||
|
||||
def __reset_navigation(self) -> None:
|
||||
self.browsing_history = History(BrowsingState.show_all())
|
||||
|
||||
def init_workers(self):
|
||||
"""Init workers for rendering thumbnails."""
|
||||
if not self.thumb_threads:
|
||||
@@ -281,7 +324,6 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.app.styleHints().setColorScheme(
|
||||
Qt.ColorScheme.Dark if self.settings.theme == Theme.DARK else Qt.ColorScheme.Light
|
||||
)
|
||||
self.applied_theme = self.settings.theme
|
||||
|
||||
if (
|
||||
platform.system() == "Darwin" or platform.system() == "Windows"
|
||||
@@ -700,7 +742,6 @@ class QtDriver(DriverMixin, QObject):
|
||||
]
|
||||
self.item_thumbs: list[ItemThumb] = []
|
||||
self.thumb_renderers: list[ThumbRenderer] = []
|
||||
self.filter = FilterState.show_all(page_size=self.settings.page_size)
|
||||
self.init_library_window()
|
||||
self.migration_modal: JsonMigrationModal = None
|
||||
|
||||
@@ -744,12 +785,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
# in a global dict for methods to access for different DPIs.
|
||||
# adj_font_size = math.floor(12 * self.main_window.devicePixelRatio())
|
||||
|
||||
def _filter_items():
|
||||
def _update_browsing_state():
|
||||
try:
|
||||
self.filter_items(
|
||||
FilterState.from_search_query(
|
||||
self.main_window.searchField.text(), page_size=self.settings.page_size
|
||||
)
|
||||
self.update_browsing_state(
|
||||
BrowsingState.from_search_query(self.main_window.searchField.text())
|
||||
.with_sorting_mode(self.sorting_mode)
|
||||
.with_sorting_direction(self.sorting_direction)
|
||||
)
|
||||
@@ -758,21 +797,21 @@ class QtDriver(DriverMixin, QObject):
|
||||
f"{Translations['status.results.invalid_syntax']} "
|
||||
f'"{self.main_window.searchField.text()}"'
|
||||
)
|
||||
logger.error("[QtDriver] Could not filter items", error=e)
|
||||
logger.error("[QtDriver] Could not update BrowsingState", error=e)
|
||||
|
||||
# Search Button
|
||||
search_button: QPushButton = self.main_window.searchButton
|
||||
search_button.clicked.connect(_filter_items)
|
||||
search_button.clicked.connect(_update_browsing_state)
|
||||
# Search Field
|
||||
search_field: QLineEdit = self.main_window.searchField
|
||||
search_field.returnPressed.connect(_filter_items)
|
||||
search_field.returnPressed.connect(_update_browsing_state)
|
||||
# Sorting Dropdowns
|
||||
sort_mode_dropdown: QComboBox = self.main_window.sorting_mode_combobox
|
||||
for sort_mode in SortingModeEnum:
|
||||
sort_mode_dropdown.addItem(Translations[sort_mode.value], sort_mode)
|
||||
sort_mode_dropdown.setCurrentIndex(
|
||||
list(SortingModeEnum).index(self.filter.sorting_mode)
|
||||
) # set according to self.filter
|
||||
list(SortingModeEnum).index(self.browsing_history.current.sorting_mode)
|
||||
) # set according to navigation state
|
||||
sort_mode_dropdown.currentIndexChanged.connect(self.sorting_mode_callback)
|
||||
|
||||
sort_dir_dropdown: QComboBox = self.main_window.sorting_direction_combobox
|
||||
@@ -794,9 +833,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
self._init_thumb_grid()
|
||||
|
||||
back_button: QPushButton = self.main_window.backButton
|
||||
back_button.clicked.connect(lambda: self.page_move(-1))
|
||||
back_button.clicked.connect(lambda: self.navigation_callback(-1))
|
||||
forward_button: QPushButton = self.main_window.forwardButton
|
||||
forward_button.clicked.connect(lambda: self.page_move(1))
|
||||
forward_button.clicked.connect(lambda: self.navigation_callback(1))
|
||||
|
||||
# NOTE: Putting this early will result in a white non-responsive
|
||||
# window until everything is loaded. Consider adding a splash screen
|
||||
@@ -805,7 +844,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.activateWindow()
|
||||
self.main_window.toggle_landing_page(enabled=True)
|
||||
|
||||
self.main_window.pagination.index.connect(lambda i: self.page_move(page_id=i))
|
||||
self.main_window.pagination.index.connect(lambda i: self.page_move(i, absolute=True))
|
||||
|
||||
self.splash.finish(self.main_window)
|
||||
|
||||
@@ -825,7 +864,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
)
|
||||
self.file_extension_panel.setTitle(Translations["ignore_list.title"])
|
||||
self.file_extension_panel.setWindowTitle(Translations["ignore_list.title"])
|
||||
self.file_extension_panel.saved.connect(lambda: (panel.save(), self.filter_items()))
|
||||
self.file_extension_panel.saved.connect(
|
||||
lambda: (panel.save(), self.update_browsing_state())
|
||||
)
|
||||
self.manage_file_ext_action.triggered.connect(self.file_extension_panel.show)
|
||||
|
||||
def show_grid_filenames(self, value: bool):
|
||||
@@ -871,7 +912,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.main_window.searchField.setText("")
|
||||
scrollbar: QScrollArea = self.main_window.scrollArea
|
||||
scrollbar.verticalScrollBar().setValue(0)
|
||||
self.filter = FilterState.show_all(page_size=self.settings.page_size)
|
||||
self.__reset_navigation()
|
||||
|
||||
self.lib.close()
|
||||
|
||||
@@ -1059,7 +1100,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.selected.clear()
|
||||
|
||||
if deleted_count > 0:
|
||||
self.filter_items()
|
||||
self.update_browsing_state()
|
||||
self.preview_panel.update_widgets()
|
||||
|
||||
if len(self.selected) <= 1 and deleted_count == 0:
|
||||
@@ -1211,7 +1252,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
pw.hide(),
|
||||
pw.deleteLater(),
|
||||
# refresh the library only when new items are added
|
||||
files_count and self.filter_items(), # type: ignore
|
||||
files_count and self.update_browsing_state(), # type: ignore
|
||||
)
|
||||
)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
@@ -1282,7 +1323,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
def sorting_direction_callback(self):
|
||||
logger.info("Sorting Direction Changed", ascending=self.sorting_direction)
|
||||
self.filter_items()
|
||||
self.update_browsing_state(
|
||||
self.browsing_history.current.with_sorting_direction(self.sorting_direction)
|
||||
)
|
||||
|
||||
@property
|
||||
def sorting_mode(self) -> SortingModeEnum:
|
||||
@@ -1291,7 +1334,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
def sorting_mode_callback(self):
|
||||
logger.info("Sorting Mode Changed", mode=self.sorting_mode)
|
||||
self.filter_items()
|
||||
self.update_browsing_state(
|
||||
self.browsing_history.current.with_sorting_mode(self.sorting_mode)
|
||||
)
|
||||
|
||||
def thumb_size_callback(self, index: int):
|
||||
"""Perform actions needed when the thumbnail size selection is changed.
|
||||
@@ -1324,41 +1369,42 @@ class QtDriver(DriverMixin, QObject):
|
||||
def mouse_navigation(self, event: QMouseEvent):
|
||||
# print(event.button())
|
||||
if event.button() == Qt.MouseButton.ForwardButton:
|
||||
self.page_move(1)
|
||||
self.navigation_callback(1)
|
||||
elif event.button() == Qt.MouseButton.BackButton:
|
||||
self.page_move(-1)
|
||||
self.navigation_callback(-1)
|
||||
|
||||
def page_move(self, delta: int = None, page_id: int = None) -> None:
|
||||
"""Navigate a step further into the navigation stack."""
|
||||
logger.info(
|
||||
"page_move",
|
||||
delta=delta,
|
||||
page_id=page_id,
|
||||
def page_move(self, value: int, absolute=False) -> None:
|
||||
logger.info("page_move", value=value, absolute=absolute)
|
||||
|
||||
if not absolute:
|
||||
value += self.browsing_history.current.page_index
|
||||
|
||||
self.browsing_history.push(
|
||||
self.browsing_history.current.with_page_index(clamp(value, 0, self.pages_count - 1))
|
||||
)
|
||||
|
||||
# Ex. User visits | A ->[B] |
|
||||
# | A B ->[C]|
|
||||
# | A [B]<- C |
|
||||
# |[A]<- B C | Previous routes still exist
|
||||
# | A ->[D] | Stack is cut from [:A] on new route
|
||||
|
||||
# sb: QScrollArea = self.main_window.scrollArea
|
||||
# sb_pos = sb.verticalScrollBar().value()
|
||||
|
||||
page_index = page_id if page_id is not None else self.filter.page_index + delta
|
||||
page_index = max(0, min(page_index, self.pages_count - 1))
|
||||
|
||||
self.filter.page_index = page_index
|
||||
# TODO: Re-allow selecting entries across multiple pages at once.
|
||||
# This works fine with additive selection but becomes a nightmare with bridging.
|
||||
self.filter_items()
|
||||
|
||||
self.update_browsing_state()
|
||||
|
||||
def navigation_callback(self, delta: int) -> None:
|
||||
"""Callback for the Forwads and Backwards Navigation Buttons next to the search bar."""
|
||||
logger.info(
|
||||
"navigation_callback",
|
||||
delta=delta,
|
||||
)
|
||||
|
||||
self.browsing_history.move(delta)
|
||||
|
||||
self.update_browsing_state()
|
||||
|
||||
def remove_grid_item(self, grid_idx: int):
|
||||
self.frame_content[grid_idx] = None
|
||||
self.item_thumbs[grid_idx].hide()
|
||||
|
||||
def _update_thumb_count(self):
|
||||
missing_count = max(0, self.filter.page_size - len(self.item_thumbs))
|
||||
missing_count = max(0, self.settings.page_size - len(self.item_thumbs))
|
||||
layout = self.flow_container.layout()
|
||||
for _ in range(missing_count):
|
||||
item_thumb = ItemThumb(
|
||||
@@ -1742,17 +1788,17 @@ class QtDriver(DriverMixin, QObject):
|
||||
pending_entries.get(badge_type, []), BADGE_TAGS[badge_type]
|
||||
)
|
||||
|
||||
def filter_items(self, filter: FilterState | None = None) -> None:
|
||||
def update_browsing_state(self, state: BrowsingState | None = None) -> None:
|
||||
"""Navigates to a new BrowsingState when state is given, otherwise updates the results."""
|
||||
if not self.lib.library_dir:
|
||||
logger.info("Library not loaded")
|
||||
return
|
||||
assert self.lib.engine
|
||||
|
||||
if filter:
|
||||
self.filter = dataclasses.replace(self.filter, **dataclasses.asdict(filter))
|
||||
else:
|
||||
self.filter.sorting_mode = self.sorting_mode
|
||||
self.filter.ascending = self.sorting_direction
|
||||
if state:
|
||||
self.browsing_history.push(state)
|
||||
|
||||
self.main_window.searchField.setText(self.browsing_history.current.query or "")
|
||||
|
||||
# inform user about running search
|
||||
self.main_window.statusbar.showMessage(Translations["status.library_search_query"])
|
||||
@@ -1760,7 +1806,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
# search the library
|
||||
start_time = time.time()
|
||||
results = self.lib.search_library(self.filter)
|
||||
results = self.lib.search_library(self.browsing_history.current, self.settings.page_size)
|
||||
logger.info("items to render", count=len(results))
|
||||
end_time = time.time()
|
||||
|
||||
@@ -1778,9 +1824,9 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.update_thumbs()
|
||||
|
||||
# update pagination
|
||||
self.pages_count = math.ceil(results.total_count / self.filter.page_size)
|
||||
self.pages_count = math.ceil(results.total_count / self.settings.page_size)
|
||||
self.main_window.pagination.update_buttons(
|
||||
self.pages_count, self.filter.page_index, emit=False
|
||||
self.pages_count, self.browsing_history.current.page_index, emit=False
|
||||
)
|
||||
|
||||
def remove_recent_library(self, item_key: str):
|
||||
@@ -1915,14 +1961,14 @@ class QtDriver(DriverMixin, QObject):
|
||||
if open_status.json_migration_req:
|
||||
self.migration_modal = JsonMigrationModal(path)
|
||||
self.migration_modal.migration_finished.connect(
|
||||
lambda: self.init_library(path, self.lib.open_library(path))
|
||||
lambda: self._init_library(path, self.lib.open_library(path))
|
||||
)
|
||||
self.main_window.landing_widget.set_status_label("")
|
||||
self.migration_modal.paged_panel.show()
|
||||
else:
|
||||
self.init_library(path, open_status)
|
||||
self._init_library(path, open_status)
|
||||
|
||||
def init_library(self, path: Path, open_status: LibraryStatus):
|
||||
def _init_library(self, path: Path, open_status: LibraryStatus):
|
||||
if not open_status.success:
|
||||
self.show_error_message(
|
||||
error_name=open_status.message
|
||||
@@ -1933,7 +1979,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
self.init_workers()
|
||||
|
||||
self.filter.page_size = self.settings.page_size
|
||||
self.__reset_navigation()
|
||||
|
||||
# TODO - make this call optional
|
||||
if self.lib.entries_count < 10000:
|
||||
@@ -1973,7 +2019,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.preview_panel.update_widgets()
|
||||
|
||||
# page (re)rendering, extract eventually
|
||||
self.filter_items()
|
||||
self.update_browsing_state()
|
||||
|
||||
self.main_window.toggle_landing_page(enabled=False)
|
||||
return open_status
|
||||
|
||||
@@ -8,7 +8,7 @@ import typing
|
||||
import structlog
|
||||
from PySide6.QtCore import Signal
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.models import Tag
|
||||
from tagstudio.qt.flowlayout import FlowLayout
|
||||
from tagstudio.qt.modals.build_tag import BuildTagPanel
|
||||
@@ -67,9 +67,7 @@ class TagBoxWidget(FieldWidget):
|
||||
tag_widget.search_for_tag_action.triggered.connect(
|
||||
lambda checked=False, tag_id=tag.id: (
|
||||
self.driver.main_window.searchField.setText(f"tag_id:{tag_id}"),
|
||||
self.driver.filter_items(
|
||||
FilterState.from_tag_id(tag_id, page_size=self.driver.settings.page_size)
|
||||
),
|
||||
self.driver.update_browsing_state(BrowsingState.from_tag_id(tag_id)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.utils.missing_files import MissingRegistry
|
||||
|
||||
@@ -28,5 +28,5 @@ def test_refresh_missing_files(library: Library):
|
||||
assert list(registry.fix_unlinked_entries()) == [0, 1]
|
||||
|
||||
# `bar.md` should be relinked to new correct path
|
||||
results = library.search_library(FilterState.from_path("bar.md", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
|
||||
assert results[0].path == Path("bar.md")
|
||||
|
||||
@@ -135,7 +135,7 @@ def test_title_update(
|
||||
qt_driver.folders_to_tags_action = QAction(menu_bar)
|
||||
|
||||
# Trigger the update
|
||||
qt_driver.init_library(library_dir, open_status)
|
||||
qt_driver._init_library(library_dir, open_status)
|
||||
|
||||
# Assert the title is updated correctly
|
||||
qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.json.library import ItemType
|
||||
from tagstudio.qt.widgets.item_thumb import ItemThumb
|
||||
|
||||
@@ -66,7 +66,7 @@ if TYPE_CHECKING:
|
||||
# assert qt_driver.selected == [0, 1, 2]
|
||||
|
||||
|
||||
def test_library_state_update(qt_driver: "QtDriver"):
|
||||
def test_browsing_state_update(qt_driver: "QtDriver"):
|
||||
# Given
|
||||
for entry in qt_driver.lib.get_entries(with_joins=True):
|
||||
thumb = ItemThumb(ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100))
|
||||
@@ -74,27 +74,25 @@ def test_library_state_update(qt_driver: "QtDriver"):
|
||||
qt_driver.frame_content.append(entry)
|
||||
|
||||
# no filter, both items are returned
|
||||
qt_driver.filter_items()
|
||||
qt_driver.update_browsing_state()
|
||||
assert len(qt_driver.frame_content) == 2
|
||||
|
||||
# filter by tag
|
||||
state = FilterState.from_tag_name("foo", page_size=10)
|
||||
qt_driver.filter_items(state)
|
||||
assert qt_driver.filter.page_size == 10
|
||||
state = BrowsingState.from_tag_name("foo")
|
||||
qt_driver.update_browsing_state(state)
|
||||
assert len(qt_driver.frame_content) == 1
|
||||
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
|
||||
assert list(entry.tags)[0].name == "foo"
|
||||
|
||||
# When state is not changed, previous one is still applied
|
||||
qt_driver.filter_items()
|
||||
assert qt_driver.filter.page_size == 10
|
||||
qt_driver.update_browsing_state()
|
||||
assert len(qt_driver.frame_content) == 1
|
||||
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
|
||||
assert list(entry.tags)[0].name == "foo"
|
||||
|
||||
# When state property is changed, previous one is overwritten
|
||||
state = FilterState.from_path("*bar.md", page_size=qt_driver.settings.page_size)
|
||||
qt_driver.filter_items(state)
|
||||
state = BrowsingState.from_path("*bar.md")
|
||||
qt_driver.update_browsing_state(state)
|
||||
assert len(qt_driver.frame_content) == 1
|
||||
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
|
||||
assert list(entry.tags)[0].name == "bar"
|
||||
|
||||
@@ -4,7 +4,7 @@ from tempfile import TemporaryDirectory
|
||||
import pytest
|
||||
|
||||
from tagstudio.core.enums import DefaultEnum, LibraryPrefs
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.fields import TextField, _FieldID
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.library.alchemy.models import Entry, Tag
|
||||
@@ -123,7 +123,8 @@ def test_library_search(library: Library, generate_tag, entry_full):
|
||||
tag = list(entry_full.tags)[0]
|
||||
|
||||
results = library.search_library(
|
||||
FilterState.from_tag_name(tag.name, page_size=500),
|
||||
BrowsingState.from_tag_name(tag.name),
|
||||
page_size=500,
|
||||
)
|
||||
|
||||
assert results.total_count == 1
|
||||
@@ -152,7 +153,7 @@ def test_entries_count(library: Library):
|
||||
new_ids = library.add_entries(entries)
|
||||
assert len(new_ids) == 10
|
||||
|
||||
results = library.search_library(FilterState.show_all(page_size=5))
|
||||
results = library.search_library(BrowsingState.show_all(), page_size=5)
|
||||
|
||||
assert results.total_count == 12
|
||||
assert len(results) == 5
|
||||
@@ -199,9 +200,7 @@ def test_search_filter_extensions(library: Library, is_exclude: bool):
|
||||
library.set_prefs(LibraryPrefs.EXTENSION_LIST, ["md"])
|
||||
|
||||
# When
|
||||
results = library.search_library(
|
||||
FilterState.show_all(page_size=500),
|
||||
)
|
||||
results = library.search_library(BrowsingState.show_all(), page_size=500)
|
||||
|
||||
# Then
|
||||
assert results.total_count == 1
|
||||
@@ -221,7 +220,8 @@ def test_search_library_case_insensitive(library: Library):
|
||||
|
||||
# When
|
||||
results = library.search_library(
|
||||
FilterState.from_tag_name(tag.name.upper(), page_size=500),
|
||||
BrowsingState.from_tag_name(tag.name.upper()),
|
||||
page_size=500,
|
||||
)
|
||||
|
||||
# Then
|
||||
@@ -443,100 +443,102 @@ def test_library_prefs_multiple_identical_vals():
|
||||
|
||||
|
||||
def test_path_search_ilike(library: Library):
|
||||
results = library.search_library(FilterState.from_path("bar.md", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
|
||||
assert results.total_count == 1
|
||||
assert len(results.items) == 1
|
||||
|
||||
|
||||
def test_path_search_like(library: Library):
|
||||
results = library.search_library(FilterState.from_path("BAR.MD", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500)
|
||||
assert results.total_count == 0
|
||||
assert len(results.items) == 0
|
||||
|
||||
|
||||
def test_path_search_default_with_sep(library: Library):
|
||||
results = library.search_library(FilterState.from_path("one/two", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("one/two"), page_size=500)
|
||||
assert results.total_count == 1
|
||||
assert len(results.items) == 1
|
||||
|
||||
|
||||
def test_path_search_glob_after(library: Library):
|
||||
results = library.search_library(FilterState.from_path("foo*", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("foo*"), page_size=500)
|
||||
assert results.total_count == 1
|
||||
assert len(results.items) == 1
|
||||
|
||||
|
||||
def test_path_search_glob_in_front(library: Library):
|
||||
results = library.search_library(FilterState.from_path("*bar.md", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("*bar.md"), page_size=500)
|
||||
assert results.total_count == 1
|
||||
assert len(results.items) == 1
|
||||
|
||||
|
||||
def test_path_search_glob_both_sides(library: Library):
|
||||
results = library.search_library(FilterState.from_path("*one/two*", page_size=500))
|
||||
results = library.search_library(BrowsingState.from_path("*one/two*"), page_size=500)
|
||||
assert results.total_count == 1
|
||||
assert len(results.items) == 1
|
||||
|
||||
|
||||
# TODO: deduplicate this code with pytest parametrisation or a for loop
|
||||
def test_path_search_ilike_glob_equality(library: Library):
|
||||
results_ilike = library.search_library(FilterState.from_path("one/two", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*one/two*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("one/two"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*one/two*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
results_ilike = library.search_library(FilterState.from_path("bar", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*bar*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("bar"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*bar*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
|
||||
# TODO: isn't this the exact same as the one before?
|
||||
def test_path_search_like_glob_equality(library: Library):
|
||||
results_ilike = library.search_library(FilterState.from_path("ONE/two", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*ONE/two*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("ONE/two"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*ONE/two*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*BAR.MD*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
results_ilike = library.search_library(FilterState.from_path("BAR.MD", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*bar.md*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("BAR.MD"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*bar.md*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
results_ilike = library.search_library(FilterState.from_path("bar.md", page_size=500))
|
||||
results_glob = library.search_library(FilterState.from_path("*BAR.MD*", page_size=500))
|
||||
results_ilike = library.search_library(BrowsingState.from_path("bar.md"), page_size=500)
|
||||
results_glob = library.search_library(BrowsingState.from_path("*BAR.MD*"), page_size=500)
|
||||
assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items]
|
||||
results_ilike, results_glob = None, None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)])
|
||||
def test_filetype_search(library: Library, filetype, num_of_filetype):
|
||||
results = library.search_library(FilterState.from_filetype(filetype, page_size=500))
|
||||
results = library.search_library(BrowsingState.from_filetype(filetype), page_size=500)
|
||||
assert len(results.items) == num_of_filetype
|
||||
|
||||
|
||||
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)])
|
||||
def test_filetype_return_one_filetype(file_mediatypes_library: Library, filetype, num_of_filetype):
|
||||
results = file_mediatypes_library.search_library(
|
||||
FilterState.from_filetype(filetype, page_size=500)
|
||||
BrowsingState.from_filetype(filetype), page_size=500
|
||||
)
|
||||
assert len(results.items) == num_of_filetype
|
||||
|
||||
|
||||
@pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)])
|
||||
def test_mediatype_search(library: Library, mediatype, num_of_mediatype):
|
||||
results = library.search_library(FilterState.from_mediatype(mediatype, page_size=500))
|
||||
results = library.search_library(BrowsingState.from_mediatype(mediatype), page_size=500)
|
||||
assert len(results.items) == num_of_mediatype
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import pytest
|
||||
|
||||
from tagstudio.core.library.alchemy.enums import FilterState
|
||||
from tagstudio.core.library.alchemy.enums import BrowsingState
|
||||
from tagstudio.core.library.alchemy.library import Library
|
||||
from tagstudio.core.query_lang.util import ParsingError
|
||||
|
||||
|
||||
def verify_count(lib: Library, query: str, count: int):
|
||||
results = lib.search_library(FilterState.from_search_query(query, page_size=500))
|
||||
results = lib.search_library(BrowsingState.from_search_query(query), page_size=500)
|
||||
assert results.total_count == count
|
||||
assert len(results.items) == count
|
||||
|
||||
@@ -136,4 +136,4 @@ def test_parent_tags(search_library: Library, query: str, count: int):
|
||||
)
|
||||
def test_syntax(search_library: Library, invalid_query: str):
|
||||
with pytest.raises(ParsingError) as e_info: # noqa: F841
|
||||
search_library.search_library(FilterState.from_search_query(invalid_query, page_size=500))
|
||||
search_library.search_library(BrowsingState.from_search_query(invalid_query), page_size=500)
|
||||
|
||||
Reference in New Issue
Block a user