diff --git a/docs/updates/roadmap.md b/docs/updates/roadmap.md index 7a8e2557..8e39b4ee 100644 --- a/docs/updates/roadmap.md +++ b/docs/updates/roadmap.md @@ -99,6 +99,7 @@ Features are broken up into the following priority levels, with nested prioritie - [ ] Fuzzy Search [LOW] [#400](https://github.com/TagStudioDev/TagStudio/issues/400) - [ ] Sortable results [HIGH] [#68](https://github.com/TagStudioDev/TagStudio/issues/68) - [ ] Sort by relevance [HIGH] + - [x] Sort by date added [HIGH] - [ ] Sort by date created [HIGH] - [ ] Sort by date modified [HIGH] - [ ] Sort by date taken (photos) [MEDIUM] @@ -182,6 +183,7 @@ These version milestones are rough estimations for when the previous core featur - [ ] Field content search [HIGH] - [ ] Sortable results [HIGH] - [ ] Sort by relevance [HIGH] + - [x] Sort by date added [HIGH] - [ ] Sort by date created [HIGH] - [ ] Sort by date modified [HIGH] - [ ] Sort by date taken (photos) [MEDIUM] diff --git a/tagstudio/resources/translations/en.json b/tagstudio/resources/translations/en.json index e9247b71..bc652b84 100644 --- a/tagstudio/resources/translations/en.json +++ b/tagstudio/resources/translations/en.json @@ -212,5 +212,7 @@ "view.size.4": "Extra Large", "window.message.error_opening_library": "Error opening library.", "window.title.error": "Error", - "window.title.open_create_library": "Open/Create Library" + "window.title.open_create_library": "Open/Create Library", + "sorting.direction.ascending": "Ascending", + "sorting.direction.descending": "Descending" } diff --git a/tagstudio/src/core/library/alchemy/enums.py b/tagstudio/src/core/library/alchemy/enums.py index 0036fbbd..6d8f3bd6 100644 --- a/tagstudio/src/core/library/alchemy/enums.py +++ b/tagstudio/src/core/library/alchemy/enums.py @@ -59,6 +59,10 @@ class ItemType(enum.Enum): TAG_GROUP = 2 +class SortingModeEnum(enum.Enum): + DATE_ADDED = "file.date_added" + + @dataclass class FilterState: """Represent a state of the Library grid view.""" @@ -66,6 +70,8 @@ class FilterState: # these should remain page_index: int | None = 0 page_size: int | None = 500 + sorting_mode: SortingModeEnum = SortingModeEnum.DATE_ADDED + ascending: bool = True # these should be erased on update # Abstract Syntax Tree Of the current Search Query @@ -110,6 +116,12 @@ class FilterState: def with_page_size(self, page_size: int) -> "FilterState": return replace(self, page_size=page_size) + def with_sorting_mode(self, mode: SortingModeEnum) -> "FilterState": + return replace(self, sorting_mode=mode) + + def with_sorting_direction(self, ascending: bool) -> "FilterState": + return replace(self, ascending=ascending) + class FieldTypeEnum(enum.Enum): TEXT_LINE = "Text Line" diff --git a/tagstudio/src/core/library/alchemy/library.py b/tagstudio/src/core/library/alchemy/library.py index 845ae3a8..d2fb77be 100644 --- a/tagstudio/src/core/library/alchemy/library.py +++ b/tagstudio/src/core/library/alchemy/library.py @@ -14,11 +14,14 @@ import structlog from humanfriendly import format_timespan from sqlalchemy import ( URL, + ColumnExpressionArgument, Engine, NullPool, and_, + asc, create_engine, delete, + desc, exists, func, or_, @@ -42,7 +45,7 @@ from ...constants import ( ) from ...enums import LibraryPrefs from .db import make_tables -from .enums import FieldTypeEnum, FilterState, TagColor +from .enums import FieldTypeEnum, FilterState, SortingModeEnum, TagColor from .fields import ( BaseField, DatetimeField, @@ -576,6 +579,13 @@ class Library: query_count = select(func.count()).select_from(statement.alias("entries")) count_all: int = session.execute(query_count).scalar() + sort_on: ColumnExpressionArgument = Entry.id + match search.sorting_mode: + case SortingModeEnum.DATE_ADDED: + sort_on = Entry.id + + statement = statement.order_by(asc(sort_on) if search.ascending else desc(sort_on)) + statement = statement.limit(search.limit).offset(search.offset) logger.info( diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index ac0280d3..513141ce 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -72,6 +72,15 @@ class Ui_MainWindow(QMainWindow): spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem) + # Sorting Dropdowns + self.sorting_mode_combobox = QComboBox(self.centralwidget) + self.sorting_mode_combobox.setObjectName(u"sortingModeComboBox") + self.horizontalLayout_3.addWidget(self.sorting_mode_combobox) + + self.sorting_direction_combobox = QComboBox(self.centralwidget) + self.sorting_direction_combobox.setObjectName(u"sortingDirectionCombobox") + self.horizontalLayout_3.addWidget(self.sorting_direction_combobox) + # Thumbnail Size placeholder self.thumb_size_combobox = QComboBox(self.centralwidget) self.thumb_size_combobox.setObjectName(u"thumbSizeComboBox") diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 6b6ee83a..7251246a 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -65,6 +65,7 @@ from src.core.library.alchemy.enums import ( FieldTypeEnum, FilterState, ItemType, + SortingModeEnum, ) from src.core.library.alchemy.fields import _FieldID from src.core.library.alchemy.library import Entry, LibraryStatus @@ -551,16 +552,40 @@ class QtDriver(DriverMixin, QObject): search_button.clicked.connect( lambda: self.filter_items( FilterState.from_search_query(self.main_window.searchField.text()) + .with_sorting_mode(self.sorting_mode) + .with_sorting_direction(self.sorting_direction) ) ) # Search Field search_field: QLineEdit = self.main_window.searchField search_field.returnPressed.connect( - # TODO - parse search field for filters lambda: self.filter_items( FilterState.from_search_query(self.main_window.searchField.text()) + .with_sorting_mode(self.sorting_mode) + .with_sorting_direction(self.sorting_direction) ) ) + # 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 + sort_mode_dropdown.currentIndexChanged.connect(self.sorting_mode_callback) + + sort_dir_dropdown: QComboBox = self.main_window.sorting_direction_combobox + sort_dir_dropdown.addItem("Ascending", userData=True) + sort_dir_dropdown.addItem("Descending", userData=False) + Translations.translate_with_setter( + lambda text: sort_dir_dropdown.setItemText(0, text), "sorting.direction.ascending" + ) + Translations.translate_with_setter( + lambda text: sort_dir_dropdown.setItemText(1, text), "sorting.direction.descending" + ) + sort_dir_dropdown.setCurrentIndex(0) # Default: Ascending + sort_dir_dropdown.currentIndexChanged.connect(self.sorting_direction_callback) + # Thumbnail Size ComboBox thumb_size_combobox: QComboBox = self.main_window.thumb_size_combobox for size in self.thumb_sizes: @@ -880,6 +905,24 @@ class QtDriver(DriverMixin, QObject): content=strip_web_protocol(field.value), ) + @property + def sorting_direction(self) -> bool: + """Whether to Sort the results in ascending order.""" + return self.main_window.sorting_direction_combobox.currentData() + + def sorting_direction_callback(self): + logger.info("Sorting Direction Changed", ascending=self.sorting_direction) + self.filter_items() + + @property + def sorting_mode(self) -> SortingModeEnum: + """What to sort by.""" + return self.main_window.sorting_mode_combobox.currentData() + + def sorting_mode_callback(self): + logger.info("Sorting Mode Changed", mode=self.sorting_mode) + self.filter_items() + def thumb_size_callback(self, index: int): """Perform actions needed when the thumbnail size selection is changed. @@ -1192,6 +1235,9 @@ class QtDriver(DriverMixin, QObject): 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 # inform user about running search self.main_window.statusbar.showMessage(Translations["status.library_search_query"])