From 3f9aa87ab64a731f9e9a5ba0e736add74b9a79cb Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:54:42 -0700 Subject: [PATCH] feat(ui): add LibraryInfoWindow with statistics (#1056) --- .../widgets/library_info_window_controller.py | 52 ++++++ src/tagstudio/qt/main_window.py | 3 + src/tagstudio/qt/ts_qt.py | 19 +- .../view/widgets/library_info_window_view.py | 165 ++++++++++++++++++ src/tagstudio/qt/widgets/migration_modal.py | 6 +- src/tagstudio/resources/translations/de.json | 6 +- src/tagstudio/resources/translations/en.json | 12 +- src/tagstudio/resources/translations/es.json | 6 +- src/tagstudio/resources/translations/fil.json | 6 +- src/tagstudio/resources/translations/fr.json | 6 +- src/tagstudio/resources/translations/hu.json | 6 +- src/tagstudio/resources/translations/ja.json | 6 +- .../resources/translations/nb_NO.json | 6 +- src/tagstudio/resources/translations/nl.json | 4 +- src/tagstudio/resources/translations/pl.json | 6 +- .../resources/translations/pt_BR.json | 6 +- src/tagstudio/resources/translations/qpv.json | 6 +- src/tagstudio/resources/translations/ru.json | 6 +- src/tagstudio/resources/translations/ta.json | 6 +- src/tagstudio/resources/translations/tok.json | 6 +- src/tagstudio/resources/translations/tr.json | 6 +- .../resources/translations/zh_Hans.json | 6 +- .../resources/translations/zh_Hant.json | 8 +- 23 files changed, 299 insertions(+), 60 deletions(-) create mode 100644 src/tagstudio/qt/controller/widgets/library_info_window_controller.py create mode 100644 src/tagstudio/qt/view/widgets/library_info_window_view.py diff --git a/src/tagstudio/qt/controller/widgets/library_info_window_controller.py b/src/tagstudio/qt/controller/widgets/library_info_window_controller.py new file mode 100644 index 00000000..db1bb0a4 --- /dev/null +++ b/src/tagstudio/qt/controller/widgets/library_info_window_controller.py @@ -0,0 +1,52 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from typing import TYPE_CHECKING + +from PySide6 import QtGui + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.translations import Translations +from tagstudio.qt.view.widgets.library_info_window_view import LibraryInfoWindowView + +# Only import for type checking/autocompletion, will not be imported at runtime. +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + + +class LibraryInfoWindow(LibraryInfoWindowView): + def __init__(self, library: "Library", driver: "QtDriver"): + super().__init__(library, driver) + + self.manage_tags_button.clicked.connect( + self.driver.main_window.menu_bar.tag_manager_action.trigger + ) + self.manage_colors_button.clicked.connect( + self.driver.main_window.menu_bar.color_manager_action.trigger + ) + self.close_button.clicked.connect(lambda: self.close()) + + def update_title(self): + assert self.lib.library_dir + title: str = Translations.format( + "library_info.title", library_dir=self.lib.library_dir.stem + ) + self.title_label.setText(f"

{title}

") + + def update_stats(self): + self.entry_count_label.setText(f"{self.lib.entries_count}") + self.tag_count_label.setText(f"{len(self.lib.tags)}") + self.field_count_label.setText(f"{len(self.lib.field_types)}") + self.namespaces_count_label.setText(f"{len(self.lib.namespaces)}") + colors_total = 0 + for c in self.lib.tag_color_groups.values(): + colors_total += len(c) + self.color_count_label.setText(f"{colors_total}") + + self.macros_count_label.setText("1") # TODO: Implement macros system + + def showEvent(self, event: QtGui.QShowEvent): # noqa N802 + self.update_title() + self.update_stats() diff --git a/src/tagstudio/qt/main_window.py b/src/tagstudio/qt/main_window.py index 59e86670..1830b940 100644 --- a/src/tagstudio/qt/main_window.py +++ b/src/tagstudio/qt/main_window.py @@ -300,6 +300,9 @@ class MainMenuBar(QMenuBar): def setup_view_menu(self): self.view_menu = QMenu(Translations["menu.view"], self) + self.library_info_action = QAction(Translations["menu.view.library_info"]) + self.view_menu.addAction(self.library_info_action) + # show_libs_list_action = QAction(Translations["settings.show_recent_libraries"], menu_bar) # show_libs_list_action.setCheckable(True) # show_libs_list_action.setChecked(self.settings.show_library_list) diff --git a/src/tagstudio/qt/ts_qt.py b/src/tagstudio/qt/ts_qt.py index e997cca6..7925a584 100644 --- a/src/tagstudio/qt/ts_qt.py +++ b/src/tagstudio/qt/ts_qt.py @@ -69,6 +69,7 @@ from tagstudio.core.utils.refresh_dir import RefreshDirTracker from tagstudio.core.utils.types import unwrap from tagstudio.core.utils.web import strip_web_protocol from tagstudio.qt.cache_manager import CacheManager +from tagstudio.qt.controller.widgets.library_info_window_controller import LibraryInfoWindow from tagstudio.qt.helpers.custom_runnable import CustomRunnable from tagstudio.qt.helpers.file_deleter import delete_file from tagstudio.qt.helpers.function_iterator import FunctionIterator @@ -180,6 +181,7 @@ class QtDriver(DriverMixin, QObject): about_modal: AboutModal unlinked_modal: FixUnlinkedEntriesModal dupe_modal: FixDupeFilesModal + library_info_window: LibraryInfoWindow applied_theme: Theme @@ -348,9 +350,8 @@ class QtDriver(DriverMixin, QObject): self.tag_manager_panel = PanelModal( widget=TagDatabasePanel(self, self.lib), title=Translations["tag_manager.title"], - done_callback=lambda s=self.selected: self.main_window.preview_panel.set_selection( - s, update_preview=False - ), + done_callback=lambda checked=False, + s=self.selected: self.main_window.preview_panel.set_selection(s, update_preview=False), has_save=False, ) @@ -454,6 +455,13 @@ class QtDriver(DriverMixin, QObject): # region View Menu ============================================================ + def create_library_info_window(): + if not hasattr(self, "library_info_window"): + self.library_info_window = LibraryInfoWindow(self.lib, self) + self.library_info_window.show() + + self.main_window.menu_bar.library_info_action.triggered.connect(create_library_info_window) + def on_show_filenames_action(checked: bool): self.settings.show_filenames_in_grid = checked self.settings.save() @@ -734,6 +742,9 @@ class QtDriver(DriverMixin, QObject): self.set_clipboard_menu_viability() self.set_select_actions_visibility() + if hasattr(self, "library_info_window"): + self.library_info_window.close() + self.main_window.preview_panel.set_selection(self.selected) self.main_window.toggle_landing_page(enabled=True) self.main_window.pagination.setHidden(True) @@ -749,6 +760,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.menu_bar.fix_dupe_files_action.setEnabled(False) self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(False) self.main_window.menu_bar.folders_to_tags_action.setEnabled(False) + self.main_window.menu_bar.library_info_action.setEnabled(False) except AttributeError: logger.warning( "[Library] Could not disable library management menu actions. Is this in a test?" @@ -1742,6 +1754,7 @@ class QtDriver(DriverMixin, QObject): self.main_window.menu_bar.fix_dupe_files_action.setEnabled(True) self.main_window.menu_bar.clear_thumb_cache_action.setEnabled(True) self.main_window.menu_bar.folders_to_tags_action.setEnabled(True) + self.main_window.menu_bar.library_info_action.setEnabled(True) self.main_window.preview_panel.set_selection(self.selected) diff --git a/src/tagstudio/qt/view/widgets/library_info_window_view.py b/src/tagstudio/qt/view/widgets/library_info_window_view.py new file mode 100644 index 00000000..0a6afc88 --- /dev/null +++ b/src/tagstudio/qt/view/widgets/library_info_window_view.py @@ -0,0 +1,165 @@ +# Copyright (C) 2025 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from typing import TYPE_CHECKING + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import ( + QGridLayout, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QSpacerItem, + QVBoxLayout, + QWidget, +) + +from tagstudio.core.library.alchemy.library import Library +from tagstudio.qt.translations import Translations + +# Only import for type checking/autocompletion, will not be imported at runtime. +if TYPE_CHECKING: + from tagstudio.qt.ts_qt import QtDriver + + +class LibraryInfoWindowView(QWidget): + def __init__(self, library: "Library", driver: "QtDriver"): + super().__init__() + self.lib = library + self.driver = driver + + self.setWindowTitle("Library Information") + self.setMinimumSize(400, 300) + self.root_layout = QVBoxLayout(self) + self.root_layout.setContentsMargins(6, 6, 6, 6) + + row_height: int = 22 + cell_alignment: Qt.AlignmentFlag = ( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter + ) + + # Title ---------------------------------------------------------------- + self.title_label = QLabel() + self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Body (Stats, etc.) --------------------------------------------------- + self.body_widget = QWidget() + self.body_layout = QHBoxLayout(self.body_widget) + self.body_layout.setContentsMargins(0, 0, 0, 0) + self.body_layout.setSpacing(0) + + # Statistics ----------------------------------------------------------- + self.stats_widget = QWidget() + self.stats_layout = QVBoxLayout(self.stats_widget) + self.stats_layout.setContentsMargins(0, 0, 0, 0) + self.stats_layout.setSpacing(12) + + self.stats_label = QLabel(f"

{Translations['library_info.stats']}

") + self.stats_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.stats_grid: QWidget = QWidget() + self.stats_grid_layout: QGridLayout = QGridLayout(self.stats_grid) + self.stats_grid_layout.setContentsMargins(0, 0, 0, 0) + self.stats_grid_layout.setSpacing(0) + self.stats_grid_layout.setColumnMinimumWidth(1, 12) + self.stats_grid_layout.setColumnMinimumWidth(3, 12) + + self.entries_row: int = 0 + self.tags_row: int = 1 + self.fields_row: int = 2 + self.namespaces_row: int = 3 + self.colors_row: int = 4 + self.macros_row: int = 5 + + self.labels_col: int = 0 + self.values_col: int = 2 + self.buttons_col: int = 4 + + self.entries_label: QLabel = QLabel(Translations["library_info.stats.entries"]) + self.entries_label.setAlignment(cell_alignment) + self.tags_label: QLabel = QLabel(Translations["library_info.stats.tags"]) + self.tags_label.setAlignment(cell_alignment) + self.fields_label: QLabel = QLabel(Translations["library_info.stats.fields"]) + self.fields_label.setAlignment(cell_alignment) + self.namespaces_label: QLabel = QLabel(Translations["library_info.stats.namespaces"]) + self.namespaces_label.setAlignment(cell_alignment) + self.colors_label: QLabel = QLabel(Translations["library_info.stats.colors"]) + self.colors_label.setAlignment(cell_alignment) + self.macros_label: QLabel = QLabel(Translations["library_info.stats.macros"]) + self.macros_label.setAlignment(cell_alignment) + + self.stats_grid_layout.addWidget(self.entries_label, self.entries_row, self.labels_col) + self.stats_grid_layout.addWidget(self.tags_label, self.tags_row, self.labels_col) + self.stats_grid_layout.addWidget(self.fields_label, self.fields_row, self.labels_col) + self.stats_grid_layout.addWidget( + self.namespaces_label, self.namespaces_row, self.labels_col + ) + self.stats_grid_layout.addWidget(self.colors_label, self.colors_row, self.labels_col) + self.stats_grid_layout.addWidget(self.macros_label, self.macros_row, self.labels_col) + + self.stats_grid_layout.setRowMinimumHeight(self.entries_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.tags_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.fields_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.namespaces_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.colors_row, row_height) + self.stats_grid_layout.setRowMinimumHeight(self.macros_row, row_height) + + self.entry_count_label: QLabel = QLabel() + self.entry_count_label.setAlignment(cell_alignment) + self.tag_count_label: QLabel = QLabel() + self.tag_count_label.setAlignment(cell_alignment) + self.field_count_label: QLabel = QLabel() + self.field_count_label.setAlignment(cell_alignment) + self.namespaces_count_label: QLabel = QLabel() + self.namespaces_count_label.setAlignment(cell_alignment) + self.color_count_label: QLabel = QLabel() + self.color_count_label.setAlignment(cell_alignment) + self.macros_count_label: QLabel = QLabel() + self.macros_count_label.setAlignment(cell_alignment) + + self.stats_grid_layout.addWidget(self.entry_count_label, self.entries_row, self.values_col) + self.stats_grid_layout.addWidget(self.tag_count_label, self.tags_row, self.values_col) + self.stats_grid_layout.addWidget(self.field_count_label, self.fields_row, self.values_col) + self.stats_grid_layout.addWidget( + self.namespaces_count_label, self.namespaces_row, self.values_col + ) + self.stats_grid_layout.addWidget(self.color_count_label, self.colors_row, self.values_col) + self.stats_grid_layout.addWidget(self.macros_count_label, self.macros_row, self.values_col) + + self.manage_tags_button = QPushButton(Translations["edit.tag_manager"]) + self.manage_colors_button = QPushButton(Translations["color_manager.title"]) + + self.stats_grid_layout.addWidget(self.manage_tags_button, self.tags_row, self.buttons_col) + self.stats_grid_layout.addWidget( + self.manage_colors_button, self.colors_row, self.buttons_col + ) + + self.stats_layout.addWidget(self.stats_label) + self.stats_layout.addWidget(self.stats_grid) + + self.body_layout.addSpacerItem( + QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + ) + self.body_layout.addWidget(self.stats_widget) + self.body_layout.addSpacerItem( + QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + ) + + # Buttons + self.button_container = QWidget() + self.button_layout = QHBoxLayout(self.button_container) + self.button_layout.setContentsMargins(6, 6, 6, 6) + self.button_layout.addStretch(1) + + self.close_button = QPushButton(Translations["generic.close"]) + self.button_layout.addWidget(self.close_button) + + # Add to root layout --------------------------------------------------- + self.root_layout.addWidget(self.title_label) + self.root_layout.addWidget(self.body_widget) + self.root_layout.addStretch(1) + self.root_layout.addStretch(2) + self.root_layout.addWidget(self.button_container) diff --git a/src/tagstudio/qt/widgets/migration_modal.py b/src/tagstudio/qt/widgets/migration_modal.py index 765c782a..3702fbb4 100644 --- a/src/tagstudio/qt/widgets/migration_modal.py +++ b/src/tagstudio/qt/widgets/migration_modal.py @@ -119,8 +119,8 @@ class JsonMigrationModal(QObject): self.match_text: str = Translations["json_migration.heading.match"] self.differ_text: str = Translations["json_migration.heading.differ"] - entries_text: str = Translations["json_migration.heading.entires"] - tags_text: str = Translations["json_migration.heading.tags"] + entries_text: str = Translations["library_info.stats.entries"] + tags_text: str = Translations["library_info.stats.tags"] names_text: str = tab + Translations["json_migration.heading.names"] shorthand_text: str = tab + Translations["json_migration.heading.shorthands"] parent_tags_text: str = tab + Translations["json_migration.heading.parent_tags"] @@ -130,7 +130,7 @@ class JsonMigrationModal(QObject): ext_type_text: str = Translations["json_migration.heading.extension_list_type"] desc_text: str = Translations["json_migration.description"] path_parity_text: str = tab + Translations["json_migration.heading.paths"] - field_parity_text: str = tab + Translations["json_migration.heading.fields"] + field_parity_text: str = tab + Translations["library_info.stats.fields"] self.entries_row: int = 0 self.path_row: int = 1 diff --git a/src/tagstudio/resources/translations/de.json b/src/tagstudio/resources/translations/de.json index c2a5e886..f7d1007b 100644 --- a/src/tagstudio/resources/translations/de.json +++ b/src/tagstudio/resources/translations/de.json @@ -150,16 +150,13 @@ "json_migration.heading.aliases": "Aliase:", "json_migration.heading.colors": "Farben:", "json_migration.heading.differ": "Diskrepanz", - "json_migration.heading.entires": "Einträge:", "json_migration.heading.extension_list_type": "Erweiterungslistentyp:", - "json_migration.heading.fields": "Felder:", "json_migration.heading.file_extension_list": "Liste der Dateiendungen:", "json_migration.heading.match": "Übereinstimmend", "json_migration.heading.names": "Namen:", "json_migration.heading.parent_tags": "Übergeordnete Tags:", "json_migration.heading.paths": "Pfade:", "json_migration.heading.shorthands": "Kurzformen:", - "json_migration.heading.tags": "Tags:", "json_migration.info.description": "Bibliotheksdaten, welche mit TagStudio-Versionen 9.4 und niedriger erstellt wurden, müssen in das neue Format v9.5+ migriert werden.

Was du wissen solltest:

", "json_migration.migrating_files_entries": "Migriere {entries:,d} Dateieinträge...", "json_migration.migration_complete": "Migration abgeschlossen!", @@ -169,6 +166,9 @@ "json_migration.title.new_lib": "

v9.5+ Bibliothek

", "json_migration.title.old_lib": "

v9.4 Bibliothek

", "landing.open_create_library": "Bibliothek öffnen/erstellen {shortcut}", + "library_info.stats.entries": "Einträge:", + "library_info.stats.fields": "Felder:", + "library_info.stats.tags": "Tags:", "library.field.add": "Feld hinzufügen", "library.field.confirm_remove": "Wollen Sie dieses \"{name}\" Feld wirklich entfernen?", "library.field.mixed_data": "Gemischte Daten", diff --git a/src/tagstudio/resources/translations/en.json b/src/tagstudio/resources/translations/en.json index 57bd2588..11b59878 100644 --- a/src/tagstudio/resources/translations/en.json +++ b/src/tagstudio/resources/translations/en.json @@ -150,16 +150,13 @@ "json_migration.heading.aliases": "Aliases:", "json_migration.heading.colors": "Colors:", "json_migration.heading.differ": "Discrepancy", - "json_migration.heading.entires": "Entries:", "json_migration.heading.extension_list_type": "Extension List Type:", - "json_migration.heading.fields": "Fields:", "json_migration.heading.file_extension_list": "File Extension List:", "json_migration.heading.match": "Matched", "json_migration.heading.names": "Names:", "json_migration.heading.parent_tags": "Parent Tags:", "json_migration.heading.paths": "Paths:", "json_migration.heading.shorthands": "Shorthands:", - "json_migration.heading.tags": "Tags:", "json_migration.info.description": "Library save files created with TagStudio versions 9.4 and below will need to be migrated to the new v9.5+ format.

What you need to know:

What's changed: