diff --git a/tagstudio/src/cli/ts_cli.py b/tagstudio/src/cli/ts_cli.py index 30e14ae9..723e8ef2 100644 --- a/tagstudio/src/cli/ts_cli.py +++ b/tagstudio/src/cli/ts_cli.py @@ -23,6 +23,7 @@ from src.core.ts_core import * from src.core.utils.web import * from src.core.utils.fs import * from src.core.library import * +from src.qt.helpers.file_opener import open_file WHITE_FG = '\033[37m' WHITE_BG = '\033[47m' @@ -352,8 +353,8 @@ class CliDriver: if not os.path.isfile(external_preview_path): temp = self.external_preview_default temp.save(external_preview_path) - if os.path.isfile(external_preview_path): - os.startfile(external_preview_path) + + open_file(external_preview_path) def set_external_preview_default(self) -> None: """Sets the external preview to its default image.""" @@ -1703,11 +1704,9 @@ class CliDriver: elif (com[0].lower() == 'open' or com[0].lower() == 'o'): if len(com) > 1: if com[1].lower() == 'location' or com[1].lower() == 'l': - args = ['explorer', '/select,', filename] - subprocess.call(args) + open_file(filename, True) else: - if os.path.isfile(filename): - os.startfile(filename) + open_file(filename) # refresh=False # self.scr_browse_entries_gallery(index) # Add Field ============================================================ @@ -2152,9 +2151,8 @@ class CliDriver: # Open ============================================================= elif (com[0].lower() == 'open' or com[0].lower() == 'o'): for match in self.lib.missing_matches[filename]: - fn = f'{os.path.normpath(self.lib.library_dir + "/" + match + "/" + entry.filename)}' - if os.path.isfile(fn): - os.startfile(fn) + fn = os.path.normpath(self.lib.library_dir + "/" + match + "/" + entry.filename) + open_file(fn) refresh = False # clear() # return self.scr_choose_missing_match(index, clear_scr=False) @@ -2274,10 +2272,9 @@ class CliDriver: elif (com[0].lower() == 'open' or com[0].lower() == 'o'): # for match in self.lib.missing_matches[filename]: # fn = f'{os.path.normpath(self.lib.library_dir + "/" + match + "/" + entry_1.filename)}' - # if os.path.isfile(fn): - # os.startfile(fn) - os.startfile(dupe[0]) - os.startfile(dupe[1]) + # open_file(fn) + open_file(dupe[0]) + open_file(dupe[1]) # clear() # return self.scr_resolve_dupe_files(index, clear_scr=False) # Mirror Entries =================================================== @@ -2385,8 +2382,7 @@ class CliDriver: # Open with Default Application ======================================== if (com[0].lower() == 'open' or com[0].lower() == 'o'): - if os.path.isfile(filename): - os.startfile(filename) + open_file(filename) # self.scr_edit_entry_tag_box(entry_index, field_index) # return # Close View =========================================================== diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 375252e6..d463dc57 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -1577,7 +1577,8 @@ class Library: # NOTE: I'd expect a blank query to return all with the other implementation, but # it misses stuff like Archive (id 0) so here's this as a catch-all. - if query == '': + query = query.strip() + if not query: all: list[int] = [] for tag in self.tags: if ignore_builtin and tag.id >= 1000: diff --git a/tagstudio/src/qt/helpers/__init__.py b/tagstudio/src/qt/helpers/__init__.py index f66a6473..a79b66ab 100644 --- a/tagstudio/src/qt/helpers/__init__.py +++ b/tagstudio/src/qt/helpers/__init__.py @@ -1,4 +1,3 @@ -from .open_file import open_file -from .file_opener import FileOpenerHelper, FileOpenerLabel +from .file_opener import open_file, FileOpenerHelper, FileOpenerLabel from .function_iterator import FunctionIterator -from .custom_runnable import CustomRunnable \ No newline at end of file +from .custom_runnable import CustomRunnable diff --git a/tagstudio/src/qt/helpers/file_opener.py b/tagstudio/src/qt/helpers/file_opener.py index ef2b6697..e30bedb0 100644 --- a/tagstudio/src/qt/helpers/file_opener.py +++ b/tagstudio/src/qt/helpers/file_opener.py @@ -5,7 +5,9 @@ import logging import os import subprocess +import shutil import sys +import traceback from PySide6.QtWidgets import QLabel from PySide6.QtCore import Qt @@ -17,7 +19,49 @@ INFO = f'[INFO]' logging.basicConfig(format="%(message)s", level=logging.INFO) -class FileOpenerHelper(): +def open_file(path: str, file_manager: bool = False): + logging.info(f'Opening file: {path}') + if not os.path.exists(path): + logging.error(f'File not found: {path}') + return + try: + if sys.platform == "win32": + normpath = os.path.normpath(path) + if file_manager: + command_name = "explorer" + command_args = [f"/select,{normpath}"] + else: + command_name = "start" + # first parameter is for title, NOT filepath + command_args = ["", normpath] + subprocess.Popen([command_name] + command_args, shell=True, close_fds=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_BREAKAWAY_FROM_JOB) + else: + if sys.platform == "darwin": + command_name = "open" + command_args = [path] + if file_manager: + # will reveal in Finder + command_args.append("-R") + else: + if file_manager: + command_name = "dbus-send" + # might not be guaranteed to launch default? + command_args = ["--session", "--dest=org.freedesktop.FileManager1", "--type=method_call", + "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1.ShowItems", + f"array:string:file://{path}", "string:"] + else: + command_name = "xdg-open" + command_args = [path] + command = shutil.which(command_name) + if command is not None: + subprocess.Popen([command] + command_args, close_fds=True) + else: + logging.info(f"Could not find {command_name} on system PATH") + except: + traceback.print_exc() + + +class FileOpenerHelper: def __init__(self, filepath:str): self.filepath = filepath @@ -25,40 +69,10 @@ class FileOpenerHelper(): self.filepath = filepath def open_file(self): - """Open the file in the default program.""" - if not os.path.exists(self.filepath): - logging.error(f'File not found: {self.filepath}') - return - - if sys.platform == 'win32': - os.startfile(self.filepath) - elif sys.platform == 'linux': - subprocess.run(['xdg-open', self.filepath]) - elif sys.platform == 'darwin': - subprocess.run(['open', self.filepath]) + open_file(self.filepath) def open_explorer(self): - """Open the file in the default file explorer.""" - if os.path.exists(self.filepath): - logging.info(f'Opening file: {self.filepath}') - if os.name == 'nt': # Windows - command = f'explorer /select,"{self.filepath}"' - subprocess.run(command, shell=True) - elif sys.platform == 'linux': - command = f'nautilus --select "{self.filepath}"' # Adjust for your Linux file manager if different - if subprocess.run(command, shell=True).returncode == 0: - file_loc = os.path.dirname(self.filepath) - file_loc = os.path.normpath(file_loc) - os.startfile(file_loc) - elif sys.platform == 'darwin': - command = f'open -R "{self.filepath}"' - result = subprocess.run(command, shell=True) - if result.returncode == 0: - logging.info('Opening file in Finder') - else: - logging.error(f'Failed to open file in Finder: {self.filepath}') - else: - logging.error(f'File not found: {self.filepath}') + open_file(self.filepath, True) class FileOpenerLabel(QLabel): diff --git a/tagstudio/src/qt/helpers/open_file.py b/tagstudio/src/qt/helpers/open_file.py deleted file mode 100644 index f2e070e4..00000000 --- a/tagstudio/src/qt/helpers/open_file.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (C) 2024 Travis Abendshien (CyanVoxel). -# Licensed under the GPL-3.0 License. -# Created for TagStudio: https://github.com/CyanVoxel/TagStudio - - -import logging -import os -import sys -import traceback -import shutil -import subprocess - - -def open_file(path: str, file_manager: bool = False): - try: - if sys.platform == "win32": - normpath = os.path.normpath(path) - if file_manager: - command_name = "explorer" - command_args = [f"/select,{normpath}"] - else: - command_name = "start" - # first parameter is for title, NOT filepath - command_args = ["", normpath] - subprocess.Popen([command_name] + command_args, shell=True, close_fds=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_BREAKAWAY_FROM_JOB) - else: - if sys.platform == "darwin": - command_name = "open" - command_args = [path] - if file_manager: - # will reveal in Finder - command_args.append("-R") - else: - if file_manager: - command_name = "dbus-send" - # might not be guaranteed to launch default? - command_args = ["--session", "--dest=org.freedesktop.FileManager1", "--type=method_call", - "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1.ShowItems", - f"array:string:file://{path}", "string:"] - else: - command_name = "xdg-open" - command_args = [path] - command = shutil.which(command_name) - if command is not None: - subprocess.Popen([command] + command_args, close_fds=True) - else: - logging.info(f"Could not find {command_name} on system PATH") - except: - traceback.print_exc() \ No newline at end of file diff --git a/tagstudio/src/qt/modals/tag_search.py b/tagstudio/src/qt/modals/tag_search.py index 59428242..7f79f8f2 100644 --- a/tagstudio/src/qt/modals/tag_search.py +++ b/tagstudio/src/qt/modals/tag_search.py @@ -27,7 +27,7 @@ class TagSearchPanel(PanelWidget): super().__init__() self.lib: Library = library # self.callback = callback - self.first_tag_id = -1 + self.first_tag_id = None self.tag_limit = 30 # self.selected_tag: int = 0 self.setMinimumSize(300, 400) @@ -68,6 +68,7 @@ class TagSearchPanel(PanelWidget): self.root_layout.addWidget(self.search_field) self.root_layout.addWidget(self.scroll_area) + self.update_tags('') # def reset(self): # self.search_field.setText('') @@ -75,7 +76,7 @@ class TagSearchPanel(PanelWidget): # self.search_field.setFocus() def on_return(self, text:str): - if text and self.first_tag_id >= 0: + if text and self.first_tag_id is not None: # callback(self.first_tag_id) self.tag_chosen.emit(self.first_tag_id) self.search_field.setText('') @@ -87,57 +88,52 @@ class TagSearchPanel(PanelWidget): def update_tags(self, query:str): # for c in self.scroll_layout.children(): # c.widget().deleteLater() - while self.scroll_layout.itemAt(0): + while self.scroll_layout.count(): # logging.info(f"I'm deleting { self.scroll_layout.itemAt(0).widget()}") self.scroll_layout.takeAt(0).widget().deleteLater() - - if query: - first_id_set = False - for tag_id in self.lib.search_tags(query, include_cluster=True)[:self.tag_limit-1]: - if not first_id_set: - self.first_tag_id = tag_id - first_id_set = True - c = QWidget() - l = QHBoxLayout(c) - l.setContentsMargins(0,0,0,0) - l.setSpacing(3) - tw = TagWidget(self.lib, self.lib.get_tag(tag_id), False, False) - ab = QPushButton() - ab.setMinimumSize(23, 23) - ab.setMaximumSize(23, 23) - ab.setText('+') - ab.setStyleSheet( - f'QPushButton{{' - f'background: {get_tag_color(ColorType.PRIMARY, self.lib.get_tag(tag_id).color)};' - # f'background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_tag_color(ColorType.PRIMARY, tag.color)}, stop:1.0 {get_tag_color(ColorType.BORDER, tag.color)});' - # f"border-color:{get_tag_color(ColorType.PRIMARY, tag.color)};" - f"color: {get_tag_color(ColorType.TEXT, self.lib.get_tag(tag_id).color)};" - f'font-weight: 600;' - f"border-color:{get_tag_color(ColorType.BORDER, self.lib.get_tag(tag_id).color)};" - f'border-radius: 6px;' - f'border-style:solid;' - f'border-width: {math.ceil(1*self.devicePixelRatio())}px;' - # f'padding-top: 1.5px;' - # f'padding-right: 4px;' - f'padding-bottom: 5px;' - # f'padding-left: 4px;' - f'font-size: 20px;' - f'}}' - f'QPushButton::hover' - f'{{' - f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};" - f"color: {get_tag_color(ColorType.DARK_ACCENT, self.lib.get_tag(tag_id).color)};" - f'background: {get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};' - f'}}') + found_tags = self.lib.search_tags(query, include_cluster=True)[:self.tag_limit - 1] + self.first_tag_id = found_tags[0] if found_tags else None - ab.clicked.connect(lambda checked=False, x=tag_id: self.tag_chosen.emit(x)) + for tag_id in found_tags: + c = QWidget() + l = QHBoxLayout(c) + l.setContentsMargins(0, 0, 0, 0) + l.setSpacing(3) + tw = TagWidget(self.lib, self.lib.get_tag(tag_id), False, False) + ab = QPushButton() + ab.setMinimumSize(23, 23) + ab.setMaximumSize(23, 23) + ab.setText('+') + ab.setStyleSheet( + f'QPushButton{{' + f'background: {get_tag_color(ColorType.PRIMARY, self.lib.get_tag(tag_id).color)};' + # f'background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {get_tag_color(ColorType.PRIMARY, tag.color)}, stop:1.0 {get_tag_color(ColorType.BORDER, tag.color)});' + # f"border-color:{get_tag_color(ColorType.PRIMARY, tag.color)};" + f"color: {get_tag_color(ColorType.TEXT, self.lib.get_tag(tag_id).color)};" + f'font-weight: 600;' + f"border-color:{get_tag_color(ColorType.BORDER, self.lib.get_tag(tag_id).color)};" + f'border-radius: 6px;' + f'border-style:solid;' + f'border-width: {math.ceil(1*self.devicePixelRatio())}px;' + # f'padding-top: 1.5px;' + # f'padding-right: 4px;' + f'padding-bottom: 5px;' + # f'padding-left: 4px;' + f'font-size: 20px;' + f'}}' + f'QPushButton::hover' + f'{{' + f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};" + f"color: {get_tag_color(ColorType.DARK_ACCENT, self.lib.get_tag(tag_id).color)};" + f'background: {get_tag_color(ColorType.LIGHT_ACCENT, self.lib.get_tag(tag_id).color)};' + f'}}') - l.addWidget(tw) - l.addWidget(ab) - self.scroll_layout.addWidget(c) - else: - self.first_tag_id = -1 + ab.clicked.connect(lambda checked=False, x=tag_id: self.tag_chosen.emit(x)) + + l.addWidget(tw) + l.addWidget(ab) + self.scroll_layout.addWidget(c) self.search_field.setFocus() diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7cc4598d..ac652b11 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -250,7 +250,9 @@ class QtDriver(QObject): file_menu.addSeparator() - file_menu.addAction(QAction('&Close Library', menu_bar)) + close_library_action = QAction('&Close Library', menu_bar) + close_library_action.triggered.connect(lambda: self.close_library()) + file_menu.addAction(close_library_action) # Edit Menu ============================================================ new_tag_action = QAction('New &Tag', menu_bar) @@ -428,6 +430,30 @@ class QtDriver(QObject): end_time = time.time() self.main_window.statusbar.showMessage(f'Library Saved! ({format_timespan(end_time - start_time)})') + def close_library(self): + if self.lib.library_dir: + # TODO: it is kinda the same code from "save_library"... + logging.info(f'Closing & Saving Library...') + self.main_window.statusbar.showMessage(f'Closed & Saving Library...') + start_time = time.time() + self.lib.save_library_to_disk() + self.settings.setValue("last_library", self.lib.library_dir) + self.settings.sync() + + self.lib.clear_internal_vars() + title_text = f'{self.base_title}' + self.main_window.setWindowTitle(title_text) + + self.nav_frames: list[NavigationState] = [] + self.cur_frame_idx: int = -1 + self.cur_query: str = '' + self.selected.clear() + self.preview_panel.update_widgets() + self.filter_items() + + end_time = time.time() + self.main_window.statusbar.showMessage(f'Library Saved and Closed! ({format_timespan(end_time - start_time)})') + def backup_library(self): logging.info(f'Backing Up Library...') self.main_window.statusbar.showMessage(f'Saving Library...') diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index f13d0a4f..be600304 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -10,7 +10,7 @@ import os from pathlib import Path import cv2 -from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance +from PIL import Image, ImageChops, UnidentifiedImageError, ImageQt, ImageDraw, ImageFont, ImageEnhance, ImageOps from PySide6.QtCore import QObject, Signal, QSize from PySide6.QtGui import QPixmap from src.core.ts_core import PLAINTEXT_TYPES, VIDEO_TYPES, IMAGE_TYPES @@ -60,9 +60,6 @@ class ThumbRenderer(QObject): ext_font = ImageFont.truetype(os.path.normpath( f'{Path(__file__).parent.parent.parent.parent}/resources/qt/fonts/Oxanium-Bold.ttf'), math.floor(12*font_pixel_ratio)) - def __init__(self): - QObject.__init__(self) - def render(self, timestamp: float, filepath, base_size: tuple[int, int], pixelRatio: float, isLoading=False): """Renders an entry/element thumbnail for the GUI.""" adj_size: int = 1 @@ -107,6 +104,8 @@ class ThumbRenderer(QObject): if image.mode != 'RGB': image = image.convert(mode='RGB') + image = ImageOps.exif_transpose(image) + # Videos ======================================================= elif extension in VIDEO_TYPES: video = cv2.VideoCapture(filepath) @@ -265,6 +264,8 @@ class ThumbRenderer(QObject): image = new_bg if image.mode != 'RGB': image = image.convert(mode='RGB') + + image = ImageOps.exif_transpose(image) # Videos ======================================================= elif extension in VIDEO_TYPES: