Skip to content

Commit 1fb1a80

Browse files
authored
fix: ui/ux parity fixes for thumbnails and files (TagStudioDev#608)
* fix(ui): display loading icon before rendered thumb * fix: skip out of range thumbs * fix: optimize library refreshing * fix(ui): tag colors show correct names * fix(ui): ensure inner field containers are deleted * fix(ui): don't show default preview label text * fix: catch all missing file thumbs; clean up logs
1 parent d152cd7 commit 1fb1a80

File tree

10 files changed

+145
-77
lines changed

10 files changed

+145
-77
lines changed

tagstudio/src/core/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
2+
# Licensed under the GPL-3.0 License.
3+
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
4+
5+
6+
class NoRendererError(Exception): ...

tagstudio/src/core/library/alchemy/library.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class Library:
131131
storage_path: Path | str | None
132132
engine: Engine | None
133133
folder: Folder | None
134+
included_files: set[Path] = set()
134135

135136
FILENAME: str = "ts_library.sqlite"
136137

@@ -140,6 +141,7 @@ def close(self):
140141
self.library_dir = None
141142
self.storage_path = None
142143
self.folder = None
144+
self.included_files = set()
143145

144146
def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus:
145147
if storage_path == ":memory:":

tagstudio/src/core/utils/refresh_dir.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99

1010
logger = structlog.get_logger(__name__)
1111

12+
GLOBAL_IGNORE_SET: set[str] = set(
13+
[
14+
TS_FOLDER_NAME,
15+
"$RECYCLE.BIN",
16+
".Trashes",
17+
".Trash",
18+
"tagstudio_thumbs",
19+
".fseventsd",
20+
".Spotlight-V100",
21+
"System Volume Information",
22+
]
23+
)
24+
1225

1326
@dataclass
1427
class RefreshDirTracker:
@@ -49,29 +62,45 @@ def refresh_dir(self, lib_path: Path) -> Iterator[int]:
4962
self.files_not_in_library = []
5063
dir_file_count = 0
5164

52-
for path in lib_path.glob("**/*"):
53-
str_path = str(path)
54-
if path.is_dir():
65+
for f in lib_path.glob("**/*"):
66+
end_time_loop = time()
67+
# Yield output every 1/30 of a second
68+
if (end_time_loop - start_time_loop) > 0.034:
69+
yield dir_file_count
70+
start_time_loop = time()
71+
72+
# Skip if the file/path is already mapped in the Library
73+
if f in self.library.included_files:
74+
dir_file_count += 1
75+
continue
76+
77+
# Ignore if the file is a directory
78+
if f.is_dir():
5579
continue
5680

57-
if "$RECYCLE.BIN" in str_path or TS_FOLDER_NAME in str_path:
81+
# Ensure new file isn't in a globally ignored folder
82+
skip: bool = False
83+
for part in f.parts:
84+
if part in GLOBAL_IGNORE_SET:
85+
skip = True
86+
break
87+
if skip:
5888
continue
5989

6090
dir_file_count += 1
61-
relative_path = path.relative_to(lib_path)
91+
self.library.included_files.add(f)
92+
93+
relative_path = f.relative_to(lib_path)
6294
# TODO - load these in batch somehow
6395
if not self.library.has_path_entry(relative_path):
6496
self.files_not_in_library.append(relative_path)
6597

66-
# Yield output every 1/30 of a second
67-
if (time() - start_time_loop) > 0.034:
68-
yield dir_file_count
69-
start_time_loop = time()
70-
7198
end_time_total = time()
99+
yield dir_file_count
72100
logger.info(
73101
"Directory scan time",
74102
path=lib_path,
75103
duration=(end_time_total - start_time_total),
76-
new_files_count=dir_file_count,
104+
files_not_in_lib=self.files_not_in_library,
105+
files_scanned=dir_file_count,
77106
)

tagstudio/src/qt/helpers/file_opener.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def open_file(path: str | Path, file_manager: bool = False):
1919
"""Open a file in the default application or file explorer.
2020
2121
Args:
22-
path (str): The path to the file to open.
23-
file_manager (bool, optional): Whether to open the file in the file manager
24-
(e.g. Finder on macOS). Defaults to False.
22+
path (str): The path to the file to open.
23+
file_manager (bool, optional): Whether to open the file in the file manager
24+
(e.g. Finder on macOS). Defaults to False.
2525
"""
2626
path = Path(path)
2727
logger.info("Opening file", path=path)
@@ -93,15 +93,15 @@ def __init__(self, filepath: str | Path):
9393
"""Initialize the FileOpenerHelper.
9494
9595
Args:
96-
filepath (str): The path to the file to open.
96+
filepath (str): The path to the file to open.
9797
"""
9898
self.filepath = str(filepath)
9999

100100
def set_filepath(self, filepath: str | Path):
101101
"""Set the filepath to open.
102102
103103
Args:
104-
filepath (str): The path to the file to open.
104+
filepath (str): The path to the file to open.
105105
"""
106106
self.filepath = str(filepath)
107107

@@ -115,20 +115,19 @@ def open_explorer(self):
115115

116116

117117
class FileOpenerLabel(QLabel):
118-
def __init__(self, text, parent=None):
118+
def __init__(self, parent=None):
119119
"""Initialize the FileOpenerLabel.
120120
121121
Args:
122-
text (str): The text to display.
123-
parent (QWidget, optional): The parent widget. Defaults to None.
122+
parent (QWidget, optional): The parent widget. Defaults to None.
124123
"""
125-
super().__init__(text, parent)
124+
super().__init__(parent)
126125

127126
def set_file_path(self, filepath):
128127
"""Set the filepath to open.
129128
130129
Args:
131-
filepath (str): The path to the file to open.
130+
filepath (str): The path to the file to open.
132131
"""
133132
self.filepath = filepath
134133

@@ -139,7 +138,7 @@ def mousePressEvent(self, event): # noqa: N802
139138
On a right click, show a context menu.
140139
141140
Args:
142-
event (QMouseEvent): The mouse press event.
141+
event (QMouseEvent): The mouse press event.
143142
"""
144143
super().mousePressEvent(event)
145144

tagstudio/src/qt/modals/build_tag.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ def __init__(self, library: Library, tag: Tag | None = None):
203203
self.color_field.setMaxVisibleItems(10)
204204
self.color_field.setStyleSheet("combobox-popup:0;")
205205
for color in TagColor:
206-
self.color_field.addItem(color.name, userData=color.value)
206+
self.color_field.addItem(color.name.replace("_", " ").title(), userData=color.value)
207207
# self.color_field.setProperty("appearance", "flat")
208208
self.color_field.currentIndexChanged.connect(
209209
lambda c: (

tagstudio/src/qt/ts_qt.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,25 +1031,41 @@ def update_thumbs(self):
10311031
self.flow_container.layout().update()
10321032
self.main_window.update()
10331033

1034-
for idx, (entry, item_thumb) in enumerate(
1035-
zip_longest(self.frame_content, self.item_thumbs)
1036-
):
1034+
is_grid_thumb = True
1035+
# Show loading placeholder icons
1036+
for entry, item_thumb in zip_longest(self.frame_content, self.item_thumbs):
10371037
if not entry:
10381038
item_thumb.hide()
10391039
continue
10401040

1041-
filepath = self.lib.library_dir / entry.path
1042-
item_thumb = self.item_thumbs[idx]
10431041
item_thumb.set_mode(ItemType.ENTRY)
10441042
item_thumb.set_item_id(entry)
10451043

10461044
# TODO - show after item is rendered
10471045
item_thumb.show()
10481046

1047+
is_loading = True
1048+
self.thumb_job_queue.put(
1049+
(
1050+
item_thumb.renderer.render,
1051+
(sys.float_info.max, "", base_size, ratio, is_loading, is_grid_thumb),
1052+
)
1053+
)
1054+
1055+
# Show rendered thumbnails
1056+
for idx, (entry, item_thumb) in enumerate(
1057+
zip_longest(self.frame_content, self.item_thumbs)
1058+
):
1059+
if not entry:
1060+
continue
1061+
1062+
filepath = self.lib.library_dir / entry.path
1063+
is_loading = False
1064+
10491065
self.thumb_job_queue.put(
10501066
(
10511067
item_thumb.renderer.render,
1052-
(sys.float_info.max, "", base_size, ratio, True, True),
1068+
(time.time(), filepath, base_size, ratio, is_loading, is_grid_thumb),
10531069
)
10541070
)
10551071

@@ -1188,7 +1204,8 @@ def open_library(self, path: Path) -> LibraryStatus:
11881204
self.filter.page_size = self.lib.prefs(LibraryPrefs.PAGE_SIZE)
11891205

11901206
# TODO - make this call optional
1191-
self.add_new_files_callback()
1207+
if self.lib.entries_count < 10000:
1208+
self.add_new_files_callback()
11921209

11931210
self.update_libs_list(path)
11941211
title_text = f"{self.base_title} - Library '{self.lib.library_dir}'"

tagstudio/src/qt/widgets/fields.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ def set_remove_callback(self, callback: Callable):
135135

136136
def set_inner_widget(self, widget: "FieldWidget"):
137137
if self.field_layout.itemAt(0):
138-
self.field_layout.itemAt(0).widget().deleteLater()
138+
old: QWidget = self.field_layout.itemAt(0).widget()
139+
self.field_layout.removeWidget(old)
140+
old.deleteLater()
141+
139142
self.field_layout.addWidget(widget)
140143

141144
def get_inner_widget(self):

tagstudio/src/qt/widgets/preview_panel.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,23 +162,27 @@ def __init__(self, library: Library, driver: "QtDriver"):
162162
image_layout.addWidget(self.preview_vid)
163163
image_layout.setAlignment(self.preview_vid, Qt.AlignmentFlag.AlignCenter)
164164
self.image_container.setMinimumSize(*self.img_button_size)
165-
self.file_label = FileOpenerLabel("filename")
165+
self.file_label = FileOpenerLabel()
166+
self.file_label.setObjectName("filenameLabel")
166167
self.file_label.setTextFormat(Qt.TextFormat.RichText)
167168
self.file_label.setWordWrap(True)
168169
self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
169170
self.file_label.setStyleSheet(file_label_style)
170171

171-
self.date_created_label = QLabel("dateCreatedLabel")
172+
self.date_created_label = QLabel()
173+
self.date_created_label.setObjectName("dateCreatedLabel")
172174
self.date_created_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
173175
self.date_created_label.setTextFormat(Qt.TextFormat.RichText)
174176
self.date_created_label.setStyleSheet(date_style)
175177

176-
self.date_modified_label = QLabel("dateModifiedLabel")
178+
self.date_modified_label = QLabel()
179+
self.date_modified_label.setObjectName("dateModifiedLabel")
177180
self.date_modified_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
178181
self.date_modified_label.setTextFormat(Qt.TextFormat.RichText)
179182
self.date_modified_label.setStyleSheet(date_style)
180183

181-
self.dimensions_label = QLabel("dimensionsLabel")
184+
self.dimensions_label = QLabel()
185+
self.dimensions_label.setObjectName("dimensionsLabel")
182186
self.dimensions_label.setWordWrap(True)
183187
self.dimensions_label.setStyleSheet(properties_style)
184188

@@ -480,7 +484,7 @@ def update_date_label(self, filepath: Path | None = None) -> None:
480484
if filepath and filepath.is_file():
481485
created: dt = None
482486
if platform.system() == "Windows" or platform.system() == "Darwin":
483-
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined]
487+
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined, unused-ignore]
484488
else:
485489
created = dt.fromtimestamp(filepath.stat().st_ctime)
486490
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)

0 commit comments

Comments
 (0)