mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-31 07:10:45 +00:00
Merge pull request #151 from yedpodtrzitko/yed/libs-sidebar
add list of libraries into sidebar
This commit is contained in:
16
tagstudio/src/core/enums.py
Normal file
16
tagstudio/src/core/enums.py
Normal 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"
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user