Skip to content

fix: ui/ux parity fixes for thumbnails and files #608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tagstudio/src/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio


class NoRendererError(Exception): ...
2 changes: 2 additions & 0 deletions tagstudio/src/core/library/alchemy/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class Library:
storage_path: Path | str | None
engine: Engine | None
folder: Folder | None
included_files: set[Path] = set()

FILENAME: str = "ts_library.sqlite"

Expand All @@ -140,6 +141,7 @@ def close(self):
self.library_dir = None
self.storage_path = None
self.folder = None
self.included_files = set()

def open_library(self, library_dir: Path, storage_path: str | None = None) -> LibraryStatus:
if storage_path == ":memory:":
Expand Down
51 changes: 40 additions & 11 deletions tagstudio/src/core/utils/refresh_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@

logger = structlog.get_logger(__name__)

GLOBAL_IGNORE_SET: set[str] = set(
[
TS_FOLDER_NAME,
"$RECYCLE.BIN",
".Trashes",
".Trash",
"tagstudio_thumbs",
".fseventsd",
".Spotlight-V100",
"System Volume Information",
]
)


@dataclass
class RefreshDirTracker:
Expand Down Expand Up @@ -49,29 +62,45 @@ def refresh_dir(self, lib_path: Path) -> Iterator[int]:
self.files_not_in_library = []
dir_file_count = 0

for path in lib_path.glob("**/*"):
str_path = str(path)
if path.is_dir():
for f in lib_path.glob("**/*"):
end_time_loop = time()
# Yield output every 1/30 of a second
if (end_time_loop - start_time_loop) > 0.034:
yield dir_file_count
start_time_loop = time()

# Skip if the file/path is already mapped in the Library
if f in self.library.included_files:
dir_file_count += 1
continue

# Ignore if the file is a directory
if f.is_dir():
continue

if "$RECYCLE.BIN" in str_path or TS_FOLDER_NAME in str_path:
# Ensure new file isn't in a globally ignored folder
skip: bool = False
for part in f.parts:
if part in GLOBAL_IGNORE_SET:
skip = True
break
if skip:
continue

dir_file_count += 1
relative_path = path.relative_to(lib_path)
self.library.included_files.add(f)

relative_path = f.relative_to(lib_path)
# TODO - load these in batch somehow
if not self.library.has_path_entry(relative_path):
self.files_not_in_library.append(relative_path)

# Yield output every 1/30 of a second
if (time() - start_time_loop) > 0.034:
yield dir_file_count
start_time_loop = time()

end_time_total = time()
yield dir_file_count
logger.info(
"Directory scan time",
path=lib_path,
duration=(end_time_total - start_time_total),
new_files_count=dir_file_count,
files_not_in_lib=self.files_not_in_library,
files_scanned=dir_file_count,
)
21 changes: 10 additions & 11 deletions tagstudio/src/qt/helpers/file_opener.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def open_file(path: str | Path, file_manager: bool = False):
"""Open a file in the default application or file explorer.

Args:
path (str): The path to the file to open.
file_manager (bool, optional): Whether to open the file in the file manager
(e.g. Finder on macOS). Defaults to False.
path (str): The path to the file to open.
file_manager (bool, optional): Whether to open the file in the file manager
(e.g. Finder on macOS). Defaults to False.
"""
path = Path(path)
logger.info("Opening file", path=path)
Expand Down Expand Up @@ -92,15 +92,15 @@ def __init__(self, filepath: str | Path):
"""Initialize the FileOpenerHelper.

Args:
filepath (str): The path to the file to open.
filepath (str): The path to the file to open.
"""
self.filepath = str(filepath)

def set_filepath(self, filepath: str | Path):
"""Set the filepath to open.

Args:
filepath (str): The path to the file to open.
filepath (str): The path to the file to open.
"""
self.filepath = str(filepath)

Expand All @@ -114,20 +114,19 @@ def open_explorer(self):


class FileOpenerLabel(QLabel):
def __init__(self, text, parent=None):
def __init__(self, parent=None):
"""Initialize the FileOpenerLabel.

Args:
text (str): The text to display.
parent (QWidget, optional): The parent widget. Defaults to None.
parent (QWidget, optional): The parent widget. Defaults to None.
"""
super().__init__(text, parent)
super().__init__(parent)

def set_file_path(self, filepath):
"""Set the filepath to open.

Args:
filepath (str): The path to the file to open.
filepath (str): The path to the file to open.
"""
self.filepath = filepath

Expand All @@ -138,7 +137,7 @@ def mousePressEvent(self, event): # noqa: N802
On a right click, show a context menu.

Args:
event (QMouseEvent): The mouse press event.
event (QMouseEvent): The mouse press event.
"""
super().mousePressEvent(event)

Expand Down
2 changes: 1 addition & 1 deletion tagstudio/src/qt/modals/build_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def __init__(self, library: Library, tag: Tag | None = None):
self.color_field.setMaxVisibleItems(10)
self.color_field.setStyleSheet("combobox-popup:0;")
for color in TagColor:
self.color_field.addItem(color.name, userData=color.value)
self.color_field.addItem(color.name.replace("_", " ").title(), userData=color.value)
# self.color_field.setProperty("appearance", "flat")
self.color_field.currentIndexChanged.connect(
lambda c: (
Expand Down
31 changes: 24 additions & 7 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,25 +1030,41 @@ def update_thumbs(self):
self.flow_container.layout().update()
self.main_window.update()

for idx, (entry, item_thumb) in enumerate(
zip_longest(self.frame_content, self.item_thumbs)
):
is_grid_thumb = True
# Show loading placeholder icons
for entry, item_thumb in zip_longest(self.frame_content, self.item_thumbs):
if not entry:
item_thumb.hide()
continue

filepath = self.lib.library_dir / entry.path
item_thumb = self.item_thumbs[idx]
item_thumb.set_mode(ItemType.ENTRY)
item_thumb.set_item_id(entry)

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

is_loading = True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this not be hardcoded? As far as I can tell this is never changed and only accessed once
(same in the next loop)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be hard-coded, but the reason I split it off into a variable is due to the way the arguments are being passed where I'm unable to use named parameters. It took some time to sort out which of the existing unnamed boolean arguments were doing what here, so I opted to store them in named variables for clarity.

self.thumb_job_queue.put(
(
item_thumb.renderer.render,
(sys.float_info.max, "", base_size, ratio, is_loading, is_grid_thumb),
)
)

# Show rendered thumbnails
for idx, (entry, item_thumb) in enumerate(
zip_longest(self.frame_content, self.item_thumbs)
):
if not entry:
continue

filepath = self.lib.library_dir / entry.path
is_loading = False

self.thumb_job_queue.put(
(
item_thumb.renderer.render,
(sys.float_info.max, "", base_size, ratio, True, True),
(time.time(), filepath, base_size, ratio, is_loading, is_grid_thumb),
)
)

Expand Down Expand Up @@ -1187,7 +1203,8 @@ def open_library(self, path: Path) -> LibraryStatus:
self.filter.page_size = self.lib.prefs(LibraryPrefs.PAGE_SIZE)

# TODO - make this call optional
self.add_new_files_callback()
if self.lib.entries_count < 10000:
self.add_new_files_callback()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change what the todo refers to? If so can it be removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the TODO here since while it originally referred to the persistent call, it still works as a TODO to add the option to customize the new threshold here.


self.update_libs_list(path)
title_text = f"{self.base_title} - Library '{self.lib.library_dir}'"
Expand Down
5 changes: 4 additions & 1 deletion tagstudio/src/qt/widgets/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ def set_remove_callback(self, callback: Callable):

def set_inner_widget(self, widget: "FieldWidget"):
if self.field_layout.itemAt(0):
self.field_layout.itemAt(0).widget().deleteLater()
old: QWidget = self.field_layout.itemAt(0).widget()
self.field_layout.removeWidget(old)
old.deleteLater()

self.field_layout.addWidget(widget)

def get_inner_widget(self):
Expand Down
14 changes: 9 additions & 5 deletions tagstudio/src/qt/widgets/preview_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,27 @@ def __init__(self, library: Library, driver: "QtDriver"):
image_layout.addWidget(self.preview_vid)
image_layout.setAlignment(self.preview_vid, Qt.AlignmentFlag.AlignCenter)
self.image_container.setMinimumSize(*self.img_button_size)
self.file_label = FileOpenerLabel("filename")
self.file_label = FileOpenerLabel()
self.file_label.setObjectName("filenameLabel")
self.file_label.setTextFormat(Qt.TextFormat.RichText)
self.file_label.setWordWrap(True)
self.file_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.file_label.setStyleSheet(file_label_style)

self.date_created_label = QLabel("dateCreatedLabel")
self.date_created_label = QLabel()
self.date_created_label.setObjectName("dateCreatedLabel")
self.date_created_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.date_created_label.setTextFormat(Qt.TextFormat.RichText)
self.date_created_label.setStyleSheet(date_style)

self.date_modified_label = QLabel("dateModifiedLabel")
self.date_modified_label = QLabel()
self.date_modified_label.setObjectName("dateModifiedLabel")
self.date_modified_label.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.date_modified_label.setTextFormat(Qt.TextFormat.RichText)
self.date_modified_label.setStyleSheet(date_style)

self.dimensions_label = QLabel("dimensionsLabel")
self.dimensions_label = QLabel()
self.dimensions_label.setObjectName("dimensionsLabel")
self.dimensions_label.setWordWrap(True)
self.dimensions_label.setStyleSheet(properties_style)

Expand Down Expand Up @@ -480,7 +484,7 @@ def update_date_label(self, filepath: Path | None = None) -> None:
if filepath and filepath.is_file():
created: dt = None
if platform.system() == "Windows" or platform.system() == "Darwin":
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined]
created = dt.fromtimestamp(filepath.stat().st_birthtime) # type: ignore[attr-defined, unused-ignore]
else:
created = dt.fromtimestamp(filepath.stat().st_ctime)
modified: dt = dt.fromtimestamp(filepath.stat().st_mtime)
Expand Down
Loading