From eede5b3600da9fcceb333a86d04246d88de3a9f0 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Fri, 10 May 2024 17:52:38 -0600 Subject: [PATCH 01/14] Move the tagstudio.ini file to alongside the executable --- tagstudio/src/qt/ts_qt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 8b286859..e03b93ee 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -188,9 +188,7 @@ class QtDriver(QObject): self.SIGTERM.connect(self.handleSIGTERM) - self.settings = QSettings( - QSettings.IniFormat, QSettings.UserScope, "tagstudio", "TagStudio" - ) + self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, "TagStudio") max_threads = os.cpu_count() for i in range(max_threads): From 74e90df6804c3dc495e637fabdeb34fca5590482 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Sun, 12 May 2024 23:07:01 -0600 Subject: [PATCH 02/14] Revert QSettings location to `IniFormat` `UserScope` defaults Adds `-c/--config-file ` argument to tag_studio.py --- tagstudio/src/qt/ts_qt.py | 40 +++++++++++++++++++++++++++++++++++---- tagstudio/tag_studio.py | 7 +++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index b3e11d45..6a18fb52 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -107,9 +107,6 @@ INFO = f"[INFO]" logging.basicConfig(format="%(message)s", level=logging.INFO) -# Keep settings in ini format in the current working directory. -QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, os.getcwd()) - class NavigationState: """Represents a state of the Library grid view.""" @@ -189,7 +186,42 @@ class QtDriver(QObject): self.SIGTERM.connect(self.handleSIGTERM) - self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, "TagStudio") + if self.args.config_file: + path = Path(self.args.config_file) + if path.is_dir(): + path = path / "TagStudio.ini" + self.settings = QSettings(str(path), QSettings.IniFormat) + logging.info( + f"[QT DRIVER] Directory provided defaulting to TagStudio.ini in directory, using {self.settings.fileName()}" + ) + elif path.is_file(): + self.settings = QSettings(str(path), QSettings.IniFormat) + logging.info( + f"[QT DRIVER] Config File exists, using {self.settings.fileName()}" + ) + else: + if path.suffix == ".ini" and path.parent.is_dir(): + self.settings = QSettings(str(path), QSettings.IniFormat) + logging.info( + f"[QT DRIVER] Config File does not exist, valid path specified using {self.settings.fileName()}" + ) + else: + self.settings = QSettings( + QSettings.IniFormat, + QSettings.UserScope, + "TagStudio", + "TagStudio", + ) + logging.warning( + f"[QT DRIVER] Config File does not exist, defaulting to {self.settings.fileName()}" + ) + else: + self.settings = QSettings( + QSettings.IniFormat, QSettings.UserScope, "TagStudio", "TagStudio" + ) + logging.info( + f"[QT DRIVER] Config File not specified, defaulting to {self.settings.fileName()}" + ) max_threads = os.cpu_count() for i in range(max_threads): diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index e98cf8c9..52a9a4b9 100644 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -29,6 +29,13 @@ def main(): type=str, help="Path to a TagStudio Library folder to open on start.", ) + parser.add_argument( + "-c", "--config-file", + dest="config_file", + type=str, + help="Path to a TagStudio.ini config file to use" + ) + # parser.add_argument('--browse', dest='browse', action='store_true', # help='Jumps to entry browsing on startup.') # parser.add_argument('--external_preview', dest='external_preview', action='store_true', From e7318c747308ffe6f9030c58943fe2d6f4dbc4e6 Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Sun, 12 May 2024 23:09:28 -0600 Subject: [PATCH 03/14] Ruff Formatting --- tagstudio/tag_studio.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index 52a9a4b9..97688343 100644 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -30,10 +30,11 @@ def main(): help="Path to a TagStudio Library folder to open on start.", ) parser.add_argument( - "-c", "--config-file", + "-c", + "--config-file", dest="config_file", type=str, - help="Path to a TagStudio.ini config file to use" + help="Path to a TagStudio.ini config file to use", ) # parser.add_argument('--browse', dest='browse', action='store_true', From 89b1921e56e739e519ded5ae183c0841f36861da Mon Sep 17 00:00:00 2001 From: Andrew Arneson Date: Mon, 13 May 2024 20:45:37 -0600 Subject: [PATCH 04/14] Eliminate guess work on config file Removes support for directory as a `--config-file` argument --- tagstudio/src/qt/ts_qt.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 6a18fb52..93020fc6 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -188,33 +188,12 @@ class QtDriver(QObject): if self.args.config_file: path = Path(self.args.config_file) - if path.is_dir(): - path = path / "TagStudio.ini" - self.settings = QSettings(str(path), QSettings.IniFormat) - logging.info( - f"[QT DRIVER] Directory provided defaulting to TagStudio.ini in directory, using {self.settings.fileName()}" + if not path.exists(): + logging.warning( + f"[QT DRIVER] Config File does not exist creating {str(path)}" ) - elif path.is_file(): - self.settings = QSettings(str(path), QSettings.IniFormat) - logging.info( - f"[QT DRIVER] Config File exists, using {self.settings.fileName()}" - ) - else: - if path.suffix == ".ini" and path.parent.is_dir(): - self.settings = QSettings(str(path), QSettings.IniFormat) - logging.info( - f"[QT DRIVER] Config File does not exist, valid path specified using {self.settings.fileName()}" - ) - else: - self.settings = QSettings( - QSettings.IniFormat, - QSettings.UserScope, - "TagStudio", - "TagStudio", - ) - logging.warning( - f"[QT DRIVER] Config File does not exist, defaulting to {self.settings.fileName()}" - ) + logging.info(f"[QT DRIVER] Using Config File {str(path)}") + self.settings = QSettings(str(path), QSettings.IniFormat) else: self.settings = QSettings( QSettings.IniFormat, QSettings.UserScope, "TagStudio", "TagStudio" From 94a0b000075a7323fc6ca4cfa9b64ce4da607916 Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Fri, 10 May 2024 01:41:38 +0800 Subject: [PATCH 05/14] add list of libraries into sidebar --- tagstudio/src/core/enums.py | 16 +++ tagstudio/src/core/ts_core.py | 2 + tagstudio/src/qt/ts_qt.py | 125 +++++++++++++++------ tagstudio/src/qt/widgets/preview_panel.py | 127 ++++++++++++++++++---- 4 files changed, 215 insertions(+), 55 deletions(-) create mode 100644 tagstudio/src/core/enums.py diff --git a/tagstudio/src/core/enums.py b/tagstudio/src/core/enums.py new file mode 100644 index 00000000..c406a523 --- /dev/null +++ b/tagstudio/src/core/enums.py @@ -0,0 +1,16 @@ +import enum + + +class SettingItems(str, enum.Enum): + """List of setting item names.""" + + START_LOAD_LAST = "start_load_last" + LAST_LIBRARY = "last_library" + LIBS_LIST = "libs_list" + WINDOW_SHOW_LIBS = "window_show_libs" + + +class Theme(str, enum.Enum): + COLOR_BG = "#65000000" + COLOR_HOVER = "#65AAAAAA" + COLOR_PRESSED = "#65EEEEEE" diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index abbfdda8..02753f5f 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -4,6 +4,7 @@ """The core classes and methods of TagStudio.""" +import enum import json import os @@ -18,6 +19,7 @@ BACKUP_FOLDER_NAME: str = "backups" COLLAGE_FOLDER_NAME: str = "collages" LIBRARY_FILENAME: str = "ts_library.json" + # TODO: Turn this whitelist into a user-configurable blacklist. IMAGE_TYPES: list[str] = [ "png", diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 8d626c52..8cac543c 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -16,7 +16,7 @@ import time import webbrowser from datetime import datetime as dt from pathlib import Path -from queue import Empty, Queue +from queue import Queue from typing import Optional from PIL import Image @@ -46,6 +46,7 @@ from PySide6.QtWidgets import ( ) from humanfriendly import format_timespan +from src.core.enums import SettingItems from src.core.library import ItemType from src.core.ts_core import ( PLAINTEXT_TYPES, @@ -88,7 +89,9 @@ from src.qt.modals.file_extension import FileExtensionModal from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal from src.qt.modals.fix_dupes import FixDupeFilesModal from src.qt.modals.folders_to_tags import FoldersToTagsModal -import src.qt.resources_rc + +# this import has side-effect of import PySide resources +import src.qt.resources_rc # pylint: disable=unused-import # SIGQUIT is not defined on Windows if sys.platform == "win32": @@ -282,6 +285,7 @@ class QtDriver(QObject): edit_menu = QMenu("&Edit", menu_bar) tools_menu = QMenu("&Tools", menu_bar) macros_menu = QMenu("&Macros", menu_bar) + window_menu = QMenu("&Window", menu_bar) help_menu = QMenu("&Help", menu_bar) # File Menu ============================================================ @@ -376,6 +380,18 @@ class QtDriver(QObject): tag_database_action.triggered.connect(lambda: self.show_tag_database()) edit_menu.addAction(tag_database_action) + check_action = QAction("Open library on start", self) + check_action.setCheckable(True) + check_action.setChecked( + self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool) + ) + check_action.triggered.connect( + lambda checked: self.settings.setValue( + SettingItems.START_LOAD_LAST, checked + ) + ) + window_menu.addAction(check_action) + # Tools Menu =========================================================== fix_unlinked_entries_action = QAction("Fix &Unlinked Entries", menu_bar) fue_modal = FixUnlinkedEntriesModal(self.lib, self) @@ -423,6 +439,20 @@ class QtDriver(QObject): macros_menu.addAction(self.sort_fields_action) folders_to_tags_action = QAction("Create Tags From Folders", menu_bar) + show_libs_list_action = QAction("Show Recent Libraries", menu_bar) + show_libs_list_action.setCheckable(True) + show_libs_list_action.setChecked( + self.settings.value(SettingItems.WINDOW_SHOW_LIBS, True, type=bool) + ) + show_libs_list_action.triggered.connect( + lambda checked: ( + self.settings.setValue(SettingItems.WINDOW_SHOW_LIBS, checked), + self.toggle_libs_list(checked), + ) + ) + window_menu.addAction(show_libs_list_action) + + folders_to_tags_action = QAction("Folders to Tags", menu_bar) ftt_modal = FoldersToTagsModal(self.lib, self) folders_to_tags_action.triggered.connect(lambda: ftt_modal.show()) macros_menu.addAction(folders_to_tags_action) @@ -440,6 +470,7 @@ class QtDriver(QObject): menu_bar.addMenu(edit_menu) menu_bar.addMenu(tools_menu) menu_bar.addMenu(macros_menu) + menu_bar.addMenu(window_menu) menu_bar.addMenu(help_menu) self.preview_panel = PreviewPanel(self.lib, self) @@ -458,6 +489,27 @@ class QtDriver(QObject): self.thumb_renderers: list[ThumbRenderer] = [] self.collation_thumb_size = math.ceil(self.thumb_size * 2) + self.init_library_window() + + lib = None + if self.args.open: + lib = self.args.open + elif self.settings.value(SettingItems.START_LOAD_LAST, True, type=bool): + lib = self.settings.value(SettingItems.LAST_LIBRARY) + + if lib: + self.splash.showMessage( + f'Opening Library "{lib}"...', + int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), + QColor("#9782ff"), + ) + self.open_library(lib) + + app.exec_() + + self.shutdown() + + def init_library_window(self): self._init_thumb_grid() # TODO: Put this into its own method that copies the font file(s) into memory @@ -510,31 +562,12 @@ class QtDriver(QObject): self.splash.finish(self.main_window) self.preview_panel.update_widgets() - # Check if a library should be opened on startup, args should override last_library - # TODO: check for behavior (open last, open default, start empty) - if ( - self.args.open - or self.settings.contains("last_library") - and os.path.isdir(self.settings.value("last_library")) - ): - if self.args.open: - lib = self.args.open - elif self.settings.value("last_library"): - lib = self.settings.value("last_library") - self.splash.showMessage( - f'Opening Library "{lib}"...', - int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter), - QColor("#9782ff"), - ) - self.open_library(lib) - - if self.args.ci: - # gracefully terminate the app in CI environment - self.thumb_job_queue.put((self.SIGTERM.emit, [])) - - app.exec() - - self.shutdown() + def toggle_libs_list(self, value: bool): + if value: + self.preview_panel.libs_flow_container.show() + else: + self.preview_panel.libs_flow_container.hide() + self.preview_panel.update() def callback_library_needed_check(self, func): """Check if loaded library has valid path before executing the button function""" @@ -548,7 +581,7 @@ class QtDriver(QObject): """Save Library on Application Exit""" if self.lib.library_dir: self.save_library() - self.settings.setValue("last_library", self.lib.library_dir) + self.settings.setValue(SettingItems.LAST_LIBRARY, self.lib.library_dir) self.settings.sync() logging.info("[SHUTDOWN] Ending Thumbnail Threads...") for _ in self.thumb_threads: @@ -597,7 +630,7 @@ class QtDriver(QObject): self.main_window.statusbar.showMessage(f"Closing & Saving Library...") start_time = time.time() self.save_library(show_status=False) - self.settings.setValue("last_library", self.lib.library_dir) + self.settings.setValue(SettingItems.LAST_LIBRARY, self.lib.library_dir) self.settings.sync() self.lib.clear_internal_vars() @@ -709,7 +742,7 @@ class QtDriver(QObject): iterator.value.connect(lambda x: pw.update_progress(x + 1)) iterator.value.connect( lambda x: pw.update_label( - f'Scanning Directories for New Files...\n{x+1} File{"s" if x+1 != 1 else ""} Searched, {len(self.lib.files_not_in_library)} New Files Found' + f'Scanning Directories for New Files...\n{x + 1} File{"s" if x + 1 != 1 else ""} Searched, {len(self.lib.files_not_in_library)} New Files Found' ) ) r = CustomRunnable(lambda: iterator.run()) @@ -760,7 +793,7 @@ class QtDriver(QObject): iterator.value.connect(lambda x: pw.update_progress(x + 1)) iterator.value.connect( lambda x: pw.update_label( - f"Running Configured Macros on {x+1}/{len(new_ids)} New Entries" + f"Running Configured Macros on {x + 1}/{len(new_ids)} New Entries" ) ) r = CustomRunnable(lambda: iterator.run()) @@ -1297,6 +1330,34 @@ class QtDriver(QObject): # self.update_thumbs() + def update_libs_list(self, path: str): + # add library to list in SettingItems.LIBS_LIST + ITEMS_LIMIT = 5 + + self.settings.beginGroup(SettingItems.LIBS_LIST) + + current_time = str(time.time()) + all_libs = {current_time: path} + + for item_key in self.settings.allKeys(): + item_path = self.settings.value(item_key) + if item_path != path: + all_libs[item_key] = item_path + + # sort items by the most recent first + all_libs = sorted(all_libs.items(), key=lambda item: item[0], reverse=True) + + # remove previously saved items + self.settings.clear() + + for idx, (item_key, item_value) in enumerate(all_libs, start=1): + if idx > ITEMS_LIMIT: + break + self.settings.setValue(item_key, item_value) + + self.settings.endGroup() + self.settings.sync() + def open_library(self, path): """Opens a TagStudio library.""" if self.lib.library_dir: @@ -1314,7 +1375,7 @@ class QtDriver(QObject): # self.lib.refresh_missing_files() # title_text = f'{self.base_title} - Library \'{self.lib.library_dir}\'' # self.main_window.setWindowTitle(title_text) - pass + self.update_libs_list(path) else: logging.info( diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 81d8c97a..935775fc 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -28,6 +28,7 @@ from PySide6.QtWidgets import ( ) from humanfriendly import format_size +from src.core.enums import SettingItems, Theme from src.core.library import Entry, ItemType, Library from src.core.ts_core import VIDEO_TYPES, IMAGE_TYPES from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file @@ -41,6 +42,7 @@ from src.qt.widgets.text_box_edit import EditTextBox from src.qt.widgets.text_line_edit import EditTextLine from src.qt.widgets.item_thumb import ItemThumb + # Only import for type checking/autocompletion, will not be imported at runtime. if typing.TYPE_CHECKING: from src.qt.ts_qt import QtDriver @@ -74,17 +76,10 @@ class PreviewPanel(QWidget): self.img_button_size: tuple[int, int] = (266, 266) self.image_ratio: float = 1.0 - root_layout = QHBoxLayout(self) - root_layout.setContentsMargins(0, 0, 0, 0) - self.image_container = QWidget() image_layout = QHBoxLayout(self.image_container) image_layout.setContentsMargins(0, 0, 0, 0) - splitter = QSplitter() - splitter.setOrientation(Qt.Orientation.Vertical) - splitter.setHandleWidth(12) - self.open_file_action = QAction("Open file", self) self.open_explorer_action = QAction("Open file in explorer", self) @@ -111,16 +106,6 @@ class PreviewPanel(QWidget): ) ) - splitter.splitterMoved.connect( - lambda: self.update_image_size( - ( - self.image_container.size().width(), - self.image_container.size().height(), - ) - ) - ) - splitter.addWidget(self.image_container) - image_layout.addWidget(self.preview_img) image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter) @@ -137,7 +122,7 @@ class PreviewPanel(QWidget): # Qt.TextInteractionFlag.TextSelectableByMouse) properties_style = ( - f"background-color:#65000000;" + f"background-color:{Theme.COLOR_BG};" f"font-family:Oxanium;" f"font-weight:bold;" f"font-size:12px;" @@ -177,19 +162,47 @@ class PreviewPanel(QWidget): # rounded corners are maintained when scrolling. I was unable to # find the right trick to only select that particular element. scroll_area.setStyleSheet( - f"QWidget#entryScrollContainer{{" - "background:#65000000;" + "QWidget#entryScrollContainer{" + f"background: {Theme.COLOR_BG};" "border-radius:6px;" - f"}}" + "}" ) scroll_area.setWidget(scroll_container) info_layout.addWidget(self.file_label) info_layout.addWidget(self.dimensions_label) info_layout.addWidget(scroll_area) - splitter.addWidget(info_section) - root_layout.addWidget(splitter) + # keep list of rendered libraries to avoid needles re-rendering + self.render_libs = set() + self.libs_layout = QVBoxLayout() + self.fill_libs_widget(self.libs_layout) + + self.libs_flow_container: QWidget = QWidget() + self.libs_flow_container.setObjectName("librariesList") + self.libs_flow_container.setLayout(self.libs_layout) + + # set initial visibility based on settings + if not self.driver.settings.value( + SettingItems.WINDOW_SHOW_LIBS, True, type=bool + ): + self.libs_flow_container.hide() + + splitter = QSplitter() + splitter.setOrientation(Qt.Orientation.Vertical) + splitter.setHandleWidth(12) + splitter.splitterMoved.connect( + lambda: self.update_image_size( + ( + self.image_container.size().width(), + self.image_container.size().height(), + ) + ) + ) + + splitter.addWidget(self.image_container) + splitter.addWidget(info_section) + splitter.addWidget(self.libs_flow_container) splitter.setStretchFactor(1, 2) self.afb_container = QWidget() @@ -208,6 +221,71 @@ class PreviewPanel(QWidget): (self.image_container.size().width(), self.image_container.size().height()) ) + root_layout = QHBoxLayout(self) + root_layout.setContentsMargins(0, 0, 0, 0) + root_layout.addWidget(splitter) + + def fill_libs_widget(self, layout: QVBoxLayout): + settings = self.driver.settings + settings.beginGroup(SettingItems.LIBS_LIST) + lib_items: dict[str, tuple[str, str]] = {} + for item_tstamp in settings.allKeys(): + val = settings.value(item_tstamp) + cut_val = val + if len(val) > 45: + cut_val = f"{val[0:10]} ... {val[-10:]}" + lib_items[item_tstamp] = (val, cut_val) + + settings.endGroup() + + new_keys = set(lib_items.keys()) + if new_keys == self.render_libs: + # no need to re-render + return + + # sort lib_items by the key + libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True) + + self.render_libs = new_keys + return self._fill_libs_widget(libs_sorted, layout) + + def _fill_libs_widget( + self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout + ): + # remove any potential previous items + for idx in reversed(range(layout.count())): + widget = layout.itemAt(idx).widget() + layout.removeWidget(widget) + # remove from GUI + widget.setParent(None) + + label = QLabel("Recent Libraries") + label.setAlignment(Qt.AlignCenter) + layout.addWidget(label) + + for tstamp, (full_val, cut_val) in libraries: + button = QPushButton(text=cut_val) + button.setObjectName(f"path{tstamp}") + + def open_library_button_clicked(path): + return lambda: self.driver.open_library(path) + + button.clicked.connect(open_library_button_clicked(full_val)) + button.setStyleSheet( + "QPushButton{" + f"background-color:{Theme.COLOR_BG};" + "border-radius:6px;" + "text-align: left;" + "padding-top: 3px;" + "padding-left: 6px;" + "padding-bottom: 4px;" + "}" + f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER};}}" + f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED};}}" + ) + button.setCursor(Qt.CursorShape.PointingHandCursor) + layout.addWidget(button) + def resizeEvent(self, event: QResizeEvent) -> None: self.update_image_size( (self.image_container.size().width(), self.image_container.size().height()) @@ -309,6 +387,9 @@ class PreviewPanel(QWidget): # self.tag_callback = tag_callback if tag_callback else None window_title = "" + # update list of libraries + self.fill_libs_widget(self.libs_layout) + # 0 Selected Items if not self.driver.selected: if self.selected or not self.initialized: From 06f528f8b5491f0483d043e4c220392389017c99 Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Tue, 14 May 2024 10:30:58 +0800 Subject: [PATCH 06/14] CR feedback --- tagstudio/src/qt/ts_qt.py | 16 +++++++--------- tagstudio/src/qt/widgets/preview_panel.py | 10 +++++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 8cac543c..217b1364 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1330,29 +1330,27 @@ class QtDriver(QObject): # self.update_thumbs() - def update_libs_list(self, path: str): - # add library to list in SettingItems.LIBS_LIST + def update_libs_list(self, path: str | Path): + """add library to list in SettingItems.LIBS_LIST""" ITEMS_LIMIT = 5 + path = Path(path) self.settings.beginGroup(SettingItems.LIBS_LIST) - current_time = str(time.time()) - all_libs = {current_time: path} + all_libs = {str(time.time()): str(path)} for item_key in self.settings.allKeys(): item_path = self.settings.value(item_key) - if item_path != path: + if Path(item_path) != path: all_libs[item_key] = item_path - # sort items by the most recent first + # sort items, most recent first all_libs = sorted(all_libs.items(), key=lambda item: item[0], reverse=True) # remove previously saved items self.settings.clear() - for idx, (item_key, item_value) in enumerate(all_libs, start=1): - if idx > ITEMS_LIMIT: - break + for item_key, item_value in all_libs[:ITEMS_LIMIT]: self.settings.setValue(item_key, item_value) self.settings.endGroup() diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 935775fc..19fef2ac 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -122,7 +122,7 @@ class PreviewPanel(QWidget): # Qt.TextInteractionFlag.TextSelectableByMouse) properties_style = ( - f"background-color:{Theme.COLOR_BG};" + f"background-color:{Theme.COLOR_BG.value};" f"font-family:Oxanium;" f"font-weight:bold;" f"font-size:12px;" @@ -163,7 +163,7 @@ class PreviewPanel(QWidget): # find the right trick to only select that particular element. scroll_area.setStyleSheet( "QWidget#entryScrollContainer{" - f"background: {Theme.COLOR_BG};" + f"background: {Theme.COLOR_BG.value};" "border-radius:6px;" "}" ) @@ -273,15 +273,15 @@ class PreviewPanel(QWidget): button.clicked.connect(open_library_button_clicked(full_val)) button.setStyleSheet( "QPushButton{" - f"background-color:{Theme.COLOR_BG};" + f"background-color:{Theme.COLOR_BG.value};" "border-radius:6px;" "text-align: left;" "padding-top: 3px;" "padding-left: 6px;" "padding-bottom: 4px;" "}" - f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER};}}" - f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED};}}" + f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER.value};}}" + f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED.value};}}" ) button.setCursor(Qt.CursorShape.PointingHandCursor) layout.addWidget(button) From a71ed7c4265dacebbf819a4cd35130888448c4b6 Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Tue, 14 May 2024 12:24:47 +0800 Subject: [PATCH 07/14] add button for removing recent lib --- tagstudio/src/core/ts_core.py | 2 - tagstudio/src/qt/ts_qt.py | 12 +++- tagstudio/src/qt/widgets/preview_panel.py | 83 ++++++++++++++++------- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 02753f5f..abbfdda8 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -4,7 +4,6 @@ """The core classes and methods of TagStudio.""" -import enum import json import os @@ -19,7 +18,6 @@ BACKUP_FOLDER_NAME: str = "backups" COLLAGE_FOLDER_NAME: str = "collages" LIBRARY_FILENAME: str = "ts_library.json" - # TODO: Turn this whitelist into a user-configurable blacklist. IMAGE_TYPES: list[str] = [ "png", diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 217b1364..eb029248 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -505,7 +505,11 @@ class QtDriver(QObject): ) self.open_library(lib) - app.exec_() + if self.args.ci: + # gracefully terminate the app in CI environment + self.thumb_job_queue.put((self.SIGTERM.emit, [])) + + app.exec() self.shutdown() @@ -1330,6 +1334,12 @@ class QtDriver(QObject): # self.update_thumbs() + def remove_recent_library(self, item_key: str): + self.settings.beginGroup(SettingItems.LIBS_LIST) + self.settings.remove(item_key) + self.settings.endGroup() + self.settings.sync() + def update_libs_list(self, path: str | Path): """add library to list in SettingItems.LIBS_LIST""" ITEMS_LIMIT = 5 diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 19fef2ac..b10fdd20 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -173,7 +173,7 @@ class PreviewPanel(QWidget): info_layout.addWidget(self.dimensions_label) info_layout.addWidget(scroll_area) - # keep list of rendered libraries to avoid needles re-rendering + # keep list of rendered libraries to avoid needless re-rendering self.render_libs = set() self.libs_layout = QVBoxLayout() self.fill_libs_widget(self.libs_layout) @@ -247,44 +247,79 @@ class PreviewPanel(QWidget): libs_sorted = sorted(lib_items.items(), key=lambda item: item[0], reverse=True) self.render_libs = new_keys - return self._fill_libs_widget(libs_sorted, layout) + self._fill_libs_widget(libs_sorted, layout) def _fill_libs_widget( self, libraries: list[tuple[str, tuple[str, str]]], layout: QVBoxLayout ): + def clear_layout(layout_item: QVBoxLayout): + for i in reversed(range(layout_item.count())): + child = layout_item.itemAt(i) + if child.widget() is not None: + child.widget().deleteLater() + elif child.layout() is not None: + clear_layout(child.layout()) + # remove any potential previous items - for idx in reversed(range(layout.count())): - widget = layout.itemAt(idx).widget() - layout.removeWidget(widget) - # remove from GUI - widget.setParent(None) + clear_layout(layout) label = QLabel("Recent Libraries") label.setAlignment(Qt.AlignCenter) - layout.addWidget(label) - for tstamp, (full_val, cut_val) in libraries: + row_layout = QHBoxLayout() + row_layout.addWidget(label) + layout.addLayout(row_layout) + + def set_button_style(btn: QPushButton, extras: list[str] | None = None): + base_style = [ + f"background-color:{Theme.COLOR_BG.value};", + "border-radius:6px;", + "text-align: left;", + "padding-top: 3px;", + "padding-left: 6px;", + "padding-bottom: 4px;", + ] + + full_style_rows = base_style + (extras or []) + + btn.setStyleSheet( + ( + "QPushButton{" + f"{''.join(full_style_rows)}" + "}" + f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER.value};}}" + f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED.value};}}" + ) + ) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + + for item_key, (full_val, cut_val) in libraries: button = QPushButton(text=cut_val) - button.setObjectName(f"path{tstamp}") + button.setObjectName(f"path{item_key}") def open_library_button_clicked(path): return lambda: self.driver.open_library(path) button.clicked.connect(open_library_button_clicked(full_val)) - button.setStyleSheet( - "QPushButton{" - f"background-color:{Theme.COLOR_BG.value};" - "border-radius:6px;" - "text-align: left;" - "padding-top: 3px;" - "padding-left: 6px;" - "padding-bottom: 4px;" - "}" - f"QPushButton::hover{{background-color:{Theme.COLOR_HOVER.value};}}" - f"QPushButton::pressed{{background-color:{Theme.COLOR_PRESSED.value};}}" - ) - button.setCursor(Qt.CursorShape.PointingHandCursor) - layout.addWidget(button) + set_button_style(button) + + button_remove = QPushButton("➖") + button_remove.setCursor(Qt.CursorShape.PointingHandCursor) + set_button_style(button_remove) + + def remove_recent_library_clicked(key: str): + return lambda: ( + self.driver.remove_recent_library(key), + self.fill_libs_widget(self.libs_layout), + ) + + button_remove.clicked.connect(remove_recent_library_clicked(item_key)) + + row_layout = QHBoxLayout() + row_layout.addWidget(button) + row_layout.addWidget(button_remove) + + layout.addLayout(row_layout) def resizeEvent(self, event: QResizeEvent) -> None: self.update_image_size( From f60a93f35be2b5f697e54192086639c7dee504f3 Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Tue, 14 May 2024 14:58:42 +0800 Subject: [PATCH 08/14] keep remove button small --- tagstudio/src/qt/widgets/preview_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index b10fdd20..9d040b38 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -305,6 +305,7 @@ class PreviewPanel(QWidget): button_remove = QPushButton("➖") button_remove.setCursor(Qt.CursorShape.PointingHandCursor) + button_remove.setFixedWidth(30) set_button_style(button_remove) def remove_recent_library_clicked(key: str): From 5d21375e65551e0b0e447c79fbdb5e68f80bf0fd Mon Sep 17 00:00:00 2001 From: yedpodtrzitko Date: Tue, 14 May 2024 15:51:41 +0800 Subject: [PATCH 09/14] dont expand recent libs --- tagstudio/src/qt/widgets/preview_panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 9d040b38..a2491624 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -181,6 +181,7 @@ class PreviewPanel(QWidget): self.libs_flow_container: QWidget = QWidget() self.libs_flow_container.setObjectName("librariesList") self.libs_flow_container.setLayout(self.libs_layout) + self.libs_flow_container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) # set initial visibility based on settings if not self.driver.settings.value( From ecea6effa4a50a968273341ac146bc27d08958e1 Mon Sep 17 00:00:00 2001 From: Xarvex Date: Wed, 15 May 2024 00:06:39 -0500 Subject: [PATCH 10/14] Release workflow with binary executables (#172) * Refactor: remove __init__ meant for Python versions before 3.3 This does mess with a large amount of imports, as the system was being misused to re-export submodules. This change is necessary if PyInstaller is to work at all. * Add MacOS icon * Create PyInstaller spec file * Create Release workflow Creates executable with PyInstaller, leveraging tag_studio.spec * Support both nonportable and portable in tag_studio.spec * Rename spec-file to create consistently-named directories * Only ignore other spec files * Swap exclusion option * Use windowed application * Ensure environment variables are strings * Cleanup visual order on GitHub interface * Use app for MacOS * Only cycle through MacOS version * All executables generated for MacOS are portable * Use up-to-date packages Should resolve caching issues * Correct architecture naming for MacOS --- .github/workflows/release.yml | 104 ++++++++++++++++++++++++++++++++++ .gitignore | 1 + tagstudio.spec | 86 ++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 tagstudio.spec diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..2b7064a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,104 @@ +name: Release + +on: + push: + tags: + - v[0-9]+.[0-9]+.[0-9]+* + +jobs: + linux: + strategy: + matrix: + build-type: ['', portable] + include: + - build-type: '' + build-flag: '' + suffix: '' + - build-type: portable + build-flag: --portable + suffix: _portable + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install -Ur requirements.txt pyinstaller + - run: pyinstaller tagstudio.spec -- ${{ matrix.build-flag }} + - run: tar czfC dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz dist tagstudio + - uses: actions/upload-artifact@v4 + with: + name: tagstudio_linux_x86_64${{ matrix.suffix }} + path: dist/tagstudio_linux_x86_64${{ matrix.suffix }}.tar.gz + + macos: + strategy: + matrix: + os-version: ['11', '14'] + include: + - os-version: '11' + arch: x86_64 + - os-version: '14' + arch: aarch64 + runs-on: macos-${{ matrix.os-version }} + env: + MACOSX_DEPLOYMENT_TARGET: '11.0' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install -Ur requirements.txt pyinstaller + - run: pyinstaller tagstudio.spec + - run: tar czfC dist/tagstudio_macos_${{ matrix.arch }}.tar.gz dist TagStudio.app + - uses: actions/upload-artifact@v4 + with: + name: tagstudio_macos_${{ matrix.arch }} + path: dist/tagstudio_macos_${{ matrix.arch }}.tar.gz + + windows: + strategy: + matrix: + build-type: ['', portable] + include: + - build-type: '' + build-flag: '' + suffix: '' + file-end: '' + - build-type: portable + build-flag: --portable + suffix: _portable + file-end: .exe + runs-on: windows-2019 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install -Ur requirements.txt pyinstaller + - run: PyInstaller tagstudio.spec -- ${{ matrix.build-flag }} + - run: Compress-Archive -Path dist/TagStudio${{ matrix.file-end }} -DestinationPath dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip + - uses: actions/upload-artifact@v4 + with: + name: tagstudio_windows_x86_64${{ matrix.suffix }} + path: dist/tagstudio_windows_x86_64${{ matrix.suffix }}.zip + + publish: + needs: [linux, macos, windows] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + - uses: softprops/action-gh-release@v2 + with: + files: | + tagstudio_linux_x86_64/* + tagstudio_linux_x86_64_portable/* + tagstudio_macos_x86_64/* + tagstudio_macos_x86_64_portable/* + tagstudio_macos_arm64/* + tagstudio_macos_arm64_portable/* + tagstudio_windows_x86_64/* + tagstudio_windows_x86_64_portable/* diff --git a/.gitignore b/.gitignore index 5f5f4002..f20f8b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ MANIFEST # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +!tagstudio.spec # Installer logs pip-log.txt diff --git a/tagstudio.spec b/tagstudio.spec new file mode 100644 index 00000000..d4b9d9fe --- /dev/null +++ b/tagstudio.spec @@ -0,0 +1,86 @@ +# -*- mode: python ; coding: utf-8 -*- +# vi: ft=python + + +from argparse import ArgumentParser +import sys +from PyInstaller.building.api import COLLECT, EXE, PYZ +from PyInstaller.building.build_main import Analysis +from PyInstaller.building.osx import BUNDLE + + +parser = ArgumentParser() +parser.add_argument('--portable', action='store_true') +options = parser.parse_args() + + +name = 'TagStudio' if sys.platform == 'win32' else 'tagstudio' +icon = None +if sys.platform == 'win32': + icon = 'tagstudio/resources/icon.ico' +elif sys.platform == 'darwin': + icon = 'tagstudio/resources/icon.icns' + + +a = Analysis( + ['tagstudio/tag_studio.py'], + pathex=[], + binaries=[], + datas=[('tagstudio/resources', 'resources')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + excludes=[], + runtime_hooks=[], + noarchive=False, + optimize=0, +) + +pyz = PYZ(a.pure) + +include = [a.scripts] +if options.portable: + include += (a.binaries, a.datas) +exe = EXE( + pyz, + *include, + [], + bootloader_ignore_signals=False, + console=False, + hide_console='hide-early', + disable_windowed_traceback=False, + debug=False, + name=name, + exclude_binaries=not options.portable, + icon=icon, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None +) + +coll = None if options.portable else COLLECT( + exe, + a.binaries, + a.datas, + name=name, + strip=False, + upx=True, + upx_exclude=[], +) + +app = BUNDLE( + exe if coll is None else coll, + name='TagStudio.app', + icon=icon, + bundle_identifier='com.github.tagstudiodev', + version='0.0.0', + info_plist={ + 'NSAppleScriptEnabled': False, + 'NSPrincipalClass': 'NSApplication', + } +) From c6d2a89263b3c4291978428c3cf193083656a1fd Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Tue, 14 May 2024 22:11:40 -0700 Subject: [PATCH 11/14] Update documentation - Update README - Update CHNAGELOG - Update `--config-file` argument help message --- CHANGELOG.md | 49 +++++++++++++++ README.md | 133 +++++++++++++++++++++------------------- tagstudio/tag_studio.py | 2 +- 3 files changed, 121 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ba3593..ab12e56c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,55 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [9.2.0-alpha] - 2024-05-14 + +### Added + +- Full macOS and Linux support +- Ability to apply tags to multiple selections at once +- Right-click context menu for opening files or their locations +- Support for all filetypes inside of the library +- Configurable filetype blacklist +- Option to automatically open last used library on startup +- Tool to convert folder structure to tag tree +- SIGTERM handling in console window +- Keyboard shortcuts for basic functions +- Basic support for plaintext thumbnails +- Default icon for files with no thumbnail support +- Menu action to close library +- All tags now show in the "Add Tag" panel by default +- Modal view to view and manage all library tags +- Build scripts for Windows and macOS +- Help menu option to visit the GitHub repository +- Toggleable "Recent Libraries" list in the entry side panel + +### Fixed + +- Fixed errors when performing actions with no library open +- Fixed bug where built-in tags were duplicated upon saving +- QThreads are now properly terminated on application exit +- Images with rotational EXIF data are now properly displayed +- Fixed "truncated" images causing errors +- Fixed images with large resolutions causing errors + +### Changed + +- Updated minimum Python version to 3.12 +- Various UI improvements + - Improved legibility of the Light Theme (still a WIP) + - Updated Dark Theme + - Added hand cursor to several clickable elements +- Fixed network paths not being able to load +- Various code cleanup and refactoring +- New application icons + +### Known Issues +- Using and editing multiple entry fields of the same type may result in incorrect field(s) being updated +- Adding Favorite or Archived tags via the thumbnail badges may apply the tag(s) to incorrect fields +- Searching for tag names with spaces does not currently function as intended + - A temporary workaround it to omit spaces in tag names when searching +- Sorting fields using the "Sort Fields" macro may result in edit icons being shown for incorrect fields + ## [9.1.0-alpha] - 2024-04-22 ### Added diff --git a/README.md b/README.md index d9d223bd..06eed578 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TagStudio (Preview/Alpha): A User-Focused Document Management System +# TagStudio (Alpha): A User-Focused Document Management System

@@ -15,6 +15,7 @@ TagStudio is a photo & file organization application with an underlying system t

## Contents + - [Goals](#goals) - [Priorities](#priorities) - [Current Features](#current-features) @@ -45,67 +46,27 @@ TagStudio is a photo & file organization application with an underlying system t - Description, Notes (Multiline Text Fields) - Tags, Meta Tags, Content Tags (Tag Boxes) - Create rich tags composed of a name, a list of aliases, and a list of “subtags” - being tags in which these tags inherit values from. -- Search for entries based on tags, metadata, or filename (using `filename: `) +- Search for entries based on tags, ~~metadata~~ (TBA), or filenames/filetypes (using `filename: `) - Special search conditions for entries that are: `untagged`/`no tags` and `empty`/`no fields`. > [!NOTE] > For more information on the project itself, please see the [FAQ](#faq) section and other docs. ## Installation -> [!CAUTION] -> TagStudio is only currently verified to work on Windows. I've run into issues with the Qt code running on Linux, but I don't know how severe these issues are. There's also likely bugs regarding filenames and portability of the databases across different OSes. -### Prerequisites +To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around. -- Python 3.12 - -### Creating the Virtual Environment - -*Skip this step if launching from the .sh script on Linux.* - -1. In the root repository directory, create a python virtual environment: - `python3 -m venv .venv` -2. Activate your environment: - -- Windows w/Powershell: `.venv\Scripts\Activate.ps1` -- Windows w/Command Prompt: `.venv\Scripts\activate.bat` -- Linux/macOS: `source .venv/bin/activate` - -3. Install the required packages: - `pip install -r requirements.txt` - -_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._ - -### Launching - -> [!NOTE] -> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3`. +Once downloaded, launch the corresponding TagStudio executable to start the program. Once open, go to **"File -> Create/Open Library"** to create your first library from a directory! #### Optional Arguments +Optional arguments to pass to the program. + > `--open ` / `-o ` > Path to a TagStudio Library folder to open on start. -#### Windows - -To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired. - -Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tag_studio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`. - -> [!CAUTION] -> TagStudio on Linux & macOS likely won't function correctly at this time. If you're trying to run this in order to help test, debug, and improve compatibility, then charge on ahead! - -#### macOS - -With the virtual environment loaded, run the python file at "tagstudio/tag_studio.py" from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`. When launching the program in the future, remember to activate the virtual environment each time before launching *(an easier method is currently being worked on).* - -#### Linux - -Run the "TagStudio.sh" script, and the program should launch! (Make sure that the script is marked as executable). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `sh TagStudio.sh`. - -##### NixOS - -Use the provided `flake.nix` file to create and enter a working environment by running `nix develop`. Then, run the above `TagStudio.sh` script. +> `--config-file ` / `-c ` +> Path to a TagStudio Library folder to open on start. ## Usage @@ -197,11 +158,56 @@ Import JSON sidecar data generated by [gallery-dl](https://github.com/mikf/galle > [!CAUTION] > This feature is not supported or documented in any official capacity whatsoever. It will likely be rolled-in to a larger and more generalized sidecar importing feature in the future. +## Creating a Development Environment + +If you're interested in contributing to TagStudio or just wish to poke around the live codebase, here are instructions for setting up the Python project. + +### Prerequisites + +- Python 3.12 + +### Creating a Python Virtual Environment + +> [!NOTE] +> Depending on your system, Python may be called `python`, `py`, `python3`, or `py3`. These instructions use the alias `python3`. You can check to see which alias you system uses and if it's for the correct Python version by typing `python3 --version` (or whichever alias) into your terminal. + +_Skip these steps if launching from the .sh script on Linux/macOS._ + +1. In the root repository directory, create a python virtual environment: + `python3 -m venv .venv` +2. Activate your environment: + +- Windows w/Powershell: `.venv\Scripts\Activate.ps1` +- Windows w/Command Prompt: `.venv\Scripts\activate.bat` +- Linux/macOS: `source .venv/bin/activate` + +3. Install the required packages: + `pip install -r requirements.txt` + +_Learn more about setting up a virtual environment [here](https://docs.python.org/3/tutorial/venv.html)._ + +### Launching Development Environment + +- **Windows** (start_win.bat) + + - To launch TagStudio, launch the `start_win.bat` file. You can modify this .bat file or create a shortcut and add one or more additional arguments if desired. + +- **Linux/macOS** (TagStudio.sh) + + - Run the "TagStudio.sh" script, and the program should launch! (Make sure that the script is marked as executable if on Linux). Note that launching from the script from outside of a terminal will not launch a terminal window with any debug or crash information. If you wish to see this information, just launch the shell script directly from your terminal with `./TagStudio.sh`. + + - **NixOS** (TagStudio.sh) + - Use the provided `flake.nix` file to create and enter a working environment by running `nix develop`. Then, run the `TagStudio.sh` script. + +- **Any** (No Scripts) + + - Alternatively, with the virtual environment loaded, run the python file at `tagstudio\tag_studio.py` from your terminal. If you're in the project's root directory, simply run `python3 tagstudio/tag_studio.py`. + ## FAQ ### What State Is the Project Currently In? -As of writing (Alpha v9.1.0) the project is in a “useable” state, however it lacks proper testing and quality of life features. Currently the program has only been verified to work properly on Windows, and is unlikely to properly run on Linux or macOS in its current state, however this functionality is a priority going forward with testers. +As of writing (Alpha v9.2.0) the project is in a useable state, however it lacks proper testing and quality of life features. ### What Features Are You Planning on Adding? @@ -214,18 +220,16 @@ Of the several features I have planned for the project, these are broken up into - Boolean Search - Coexisting Text + Tag Search - Searchable File Metadata -- Tag management view -- Applying metadata via multi-selection +- Comprehensive Tag management tab - Easier ways to apply tags in bulk - Tag Search Panel - Recent Tags Panel - Top Tags Panel - Pinned Tags Panel -- Apply tags based on system folders - Better (stable, performant) library grid view - Improved entry relinking - Cached thumbnails -- Collations +- Tag-like Groups - Resizable thumbnail grid - User-defined metadata fields - Multiple directory support @@ -235,13 +239,13 @@ Of the several features I have planned for the project, these are broken up into - Better internal API for accessing Entries, Tags, Fields, etc. from the library. - Proper testing workflow - Continued code cleanup and modularization -- Reassessment of save file structure in order to prioritize portability (leading to exportable tags, presets, etc) +- Exportable/importable library data including "Tag Packs" #### Future Features - Support for multiple simultaneous users/clients - Draggable files outside the program -- Ability to ignore specific files +- Comprehensive filetype whitelist - A finished “macro system” for automatic tagging based on predetermined criteria. - Different library views - Date and time fields @@ -249,20 +253,20 @@ Of the several features I have planned for the project, these are broken up into - Audio waveform previews - 3D object previews - Additional previews for miscellaneous file types -- Exportable/sharable tags and settings - Optional global tags and settings, spanning across libraries - Importing & exporting libraries to/from other programs - Port to a more performant language and modern frontend (Rust?, Tauri?, etc.) - Plugin system - Local OCR search - Support for local machine learning-based tag suggestions for images -- Mobile version +- Mobile version _(FAR future)_ #### Features I Likely Won’t Add/Pull -- Native Cloud Integration + +- Native Cloud Integration - There are plenty of services already (native or third-party) that allow you to mount your cloud drives as virtual drives on your system. Pointing TagStudio to one of these mounts should function similarly to what native integration would look like. - Native ChatGPT/Non-Local LLM Integration - - This could mean different things depending on what you're intending. Whether it's trying to use an LLM to replace the native search, or to trying to use a model for image recognition, I'm not interested in hooking people's TagStudio libraries into non-local LLMs such as ChatGPT and/or turn the program into a "chatbot" interface (see: [Goals/Privacy](#goals)). I wouldn't, however, mind using **locally** hosted models to provide the *optional* ability for additional searching and tagging methods (especially when it comes to facial recognition). + - This could mean different things depending on what you're intending. Whether it's trying to use an LLM to replace the native search, or to trying to use a model for image recognition, I'm not interested in hooking people's TagStudio libraries into non-local LLMs such as ChatGPT and/or turn the program into a "chatbot" interface (see: [Goals/Privacy](#goals)). I wouldn't, however, mind using **locally** hosted models to provide the _optional_ ability for additional searching and tagging methods (especially when it comes to facial recognition). ### Why Is the Version Already v9? @@ -270,14 +274,19 @@ I’ve been developing this project over several years in private, and have gone ### Wait, Is There a CLI Version? -As of right now, no. However, I _did_ have a CLI version in the recent past before dedicating my efforts to the Qt GUI version. I’ve left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, it’s just a bunch of glorified print statements (_the outlook for some form of curses on Windows didn’t look great at the time, and I just needed a driver for the newly refactored code...)._ +As of right now, **no**. However, I _did_ have a CLI version in the recent past before dedicating my efforts to the Qt GUI version. I’ve left in the currently-inoperable CLI code just in case anyone was curious about it. Also yes, it’s just a bunch of glorified print statements (_the outlook for some form of curses on Windows didn’t look great at the time, and I just needed a driver for the newly refactored code...)._ ### Can I Contribute? **Yes!!** I recommend taking a look at the [Priority Features](#priority-features), [Future Features](#future-features), and [Features I Won't Pull](#features-i-likely-wont-addpull) lists, as well as the project issues to see what’s currently being worked on. Please do not submit pull requests with new feature additions without opening up an issue with a feature request first. -Code formatting is automatically checked via [ruff](https://docs.astral.sh/ruff/). +Code formatting is automatically checked via [Ruff](https://docs.astral.sh/ruff/). -To format the code manually, install ruff via `pip install -r requirements-dev.txt` and then run `ruff format` +To format the code manually, install ruff via `pip install -r requirements-dev.txt` and then run `ruff format` To format the code automatically before each commit, there's a configured action available for `pre-commit` hook. Install it by running `pre-commit install`. The hook will be executed each time on running `git commit`. + +More structured documentation on contribution requirements is on its way, but for now: + +- Use `pathlib` in favor of `os.path` +- Try to make new UI additions match the existing style of the application diff --git a/tagstudio/tag_studio.py b/tagstudio/tag_studio.py index b80f2e01..cfec27c5 100644 --- a/tagstudio/tag_studio.py +++ b/tagstudio/tag_studio.py @@ -34,7 +34,7 @@ def main(): "--config-file", dest="config_file", type=str, - help="Path to a TagStudio.ini config file to use", + help="Path to a TagStudio .ini or .plist config file to use.", ) # parser.add_argument('--browse', dest='browse', action='store_true', From e655fd091dd965832abb34b72bcffa2bc9d17f14 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Tue, 14 May 2024 22:27:15 -0700 Subject: [PATCH 12/14] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab12e56c..e65520b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [9.2.0-alpha] - 2024-05-14 +## [9.2.0] - 2024-05-14 ### Added @@ -54,7 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A temporary workaround it to omit spaces in tag names when searching - Sorting fields using the "Sort Fields" macro may result in edit icons being shown for incorrect fields -## [9.1.0-alpha] - 2024-04-22 +## [9.1.0] - 2024-04-22 ### Added From 296aed6575645582e8bbe91ce3888193795c51f5 Mon Sep 17 00:00:00 2001 From: Xarvex Date: Wed, 15 May 2024 00:45:19 -0500 Subject: [PATCH 13/14] Correct upload binaries used in release workflow --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b7064a9..5ea9e2ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,8 +97,6 @@ jobs: tagstudio_linux_x86_64/* tagstudio_linux_x86_64_portable/* tagstudio_macos_x86_64/* - tagstudio_macos_x86_64_portable/* - tagstudio_macos_arm64/* - tagstudio_macos_arm64_portable/* + tagstudio_macos_aarch64/* tagstudio_windows_x86_64/* tagstudio_windows_x86_64_portable/* From e814d09c60e7be03b734cbfdc97378e82d3f1a3f Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 15 May 2024 00:15:44 -0700 Subject: [PATCH 14/14] Add macOS Gatekeeper note to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06eed578..4ec61240 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ TagStudio is a photo & file organization application with an underlying system t To download TagStudio, visit the [Releases](https://github.com/TagStudioDev/TagStudio/releases) section of the GitHub repository and download the latest release for your system. TagStudio is available for **Windows**, **macOS** _(Apple Silicon & Intel)_, and **Linux**. Windows and Linux builds are also available in portable versions if you want a more self-contained executable to move around. -Once downloaded, launch the corresponding TagStudio executable to start the program. Once open, go to **"File -> Create/Open Library"** to create your first library from a directory! +> [!NOTE] +> On macOS, you may be met with a message saying _""TagStudio" can't be opened because Apple cannot check it for malicious software."_ If you encounter this, then you'll need to go to the "Settings" app, navigate to "Privacy & Security", and scroll down to a section that says _""TagStudio" was blocked from use because it is not from an identified developer."_ Click the "Open Anyway" button to allow TagStudio to run. You should only have to do this once after downloading the application. #### Optional Arguments