Merge pull request #151 from yedpodtrzitko/yed/libs-sidebar

add list of libraries into sidebar
This commit is contained in:
Travis Abendshien
2024-05-14 12:06:42 -07:00
committed by GitHub
3 changed files with 258 additions and 55 deletions

View File

@@ -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"

View File

@@ -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,31 @@ 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)
if self.args.ci:
# gracefully terminate the app in CI environment
self.thumb_job_queue.put((self.SIGTERM.emit, []))
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 +566,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 +585,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 +634,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 +746,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 +797,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 +1334,38 @@ 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
path = Path(path)
self.settings.beginGroup(SettingItems.LIBS_LIST)
all_libs = {str(time.time()): str(path)}
for item_key in self.settings.allKeys():
item_path = self.settings.value(item_key)
if Path(item_path) != path:
all_libs[item_key] = item_path
# 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 item_key, item_value in all_libs[:ITEMS_LIMIT]:
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 +1383,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(

View File

@@ -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.value};"
f"font-family:Oxanium;"
f"font-weight:bold;"
f"font-size:12px;"
@@ -177,19 +162,48 @@ 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.value};"
"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 needless 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)
self.libs_flow_container.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
# 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 +222,107 @@ 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
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
clear_layout(layout)
label = QLabel("Recent Libraries")
label.setAlignment(Qt.AlignCenter)
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{item_key}")
def open_library_button_clicked(path):
return lambda: self.driver.open_library(path)
button.clicked.connect(open_library_button_clicked(full_val))
set_button_style(button)
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):
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(
(self.image_container.size().width(), self.image_container.size().height())
@@ -309,6 +424,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: