diff --git a/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png b/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png new file mode 100644 index 00000000..a5d33347 Binary files /dev/null and b/tagstudio/resources/qt/images/tagstudio_logo_text_mono.png differ diff --git a/tagstudio/src/qt/helpers/color_overlay.py b/tagstudio/src/qt/helpers/color_overlay.py new file mode 100644 index 00000000..c19ba73e --- /dev/null +++ b/tagstudio/src/qt/helpers/color_overlay.py @@ -0,0 +1,59 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +from PIL import Image +from PySide6.QtCore import Qt +from PySide6.QtGui import QGuiApplication +from src.qt.helpers.gradient import linear_gradient + +# TODO: Consolidate the built-in QT theme values with the values +# here, in enums.py, and in palette.py. +_THEME_DARK_FG: str = "#FFFFFF55" +_THEME_LIGHT_FG: str = "#000000DD" + + +def theme_fg_overlay(image: Image.Image) -> Image.Image: + """ + Overlay the foreground theme color onto an image. + + Args: + image (Image): The PIL Image object to apply an overlay to. + """ + + overlay_color = ( + _THEME_DARK_FG + if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark + else _THEME_LIGHT_FG + ) + im = Image.new(mode="RGBA", size=image.size, color=overlay_color) + return _apply_overlay(image, im) + + +def gradient_overlay(image: Image.Image, gradient=list[str]) -> Image.Image: + """ + Overlay a color gradient onto an image. + + Args: + image (Image): The PIL Image object to apply an overlay to. + gradient (list[str): A list of string hex color codes for use as + the colors of the gradient. + """ + + im: Image.Image = _apply_overlay(image, linear_gradient(image.size, gradient)) + return im + + +def _apply_overlay(image: Image.Image, overlay: Image.Image) -> Image.Image: + """ + Internal method to apply an overlay on top of an image, using + the image's alpha channel as a mask. + + Args: + image (Image): The PIL Image object to apply an overlay to. + overlay (Image): The PIL Image object to act as the overlay contents. + """ + im: Image.Image = Image.new(mode="RGBA", size=image.size, color="#00000000") + im.paste(overlay, (0, 0), mask=image) + return im diff --git a/tagstudio/src/qt/helpers/gradient.py b/tagstudio/src/qt/helpers/gradient.py index b346d71a..dabe7639 100644 --- a/tagstudio/src/qt/helpers/gradient.py +++ b/tagstudio/src/qt/helpers/gradient.py @@ -5,7 +5,9 @@ from PIL import Image, ImageEnhance, ImageChops -def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl): +def four_corner_gradient_background( + image: Image.Image, adj_size, mask, hl +) -> Image.Image: if image.size != (adj_size, adj_size): # Old 1 color method. # bg_col = image.copy().resize((1, 1)).getpixel((0,0)) @@ -48,3 +50,16 @@ def four_corner_gradient_background(image: Image.Image, adj_size, mask, hl): hl_soft.putalpha(ImageEnhance.Brightness(hl.getchannel(3)).enhance(0.5)) final.paste(ImageChops.soft_light(final, hl_soft), mask=hl_soft.getchannel(3)) return final + + +def linear_gradient( + size=tuple[int, int], + colors=list[str], + interpolation: Image.Resampling = Image.Resampling.BICUBIC, +) -> Image.Image: + seed: Image.Image = Image.new(mode="RGBA", size=(len(colors), 1), color="#000000") + for i, color in enumerate(colors): + c_im: Image.Image = Image.new(mode="RGBA", size=(1, 1), color=color) + seed.paste(c_im, (i, 0)) + gradient: Image.Image = seed.resize(size, resample=interpolation) + return gradient diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index 1eb2053b..a77f8744 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -12,256 +12,227 @@ # Licensed under the GPL-3.0 License. # Created for TagStudio: https://github.com/CyanVoxel/TagStudio -from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect, - QSize, Qt) -from PySide6.QtGui import (QFont, QAction) + +import logging +import typing +from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,QSize, Qt) +from PySide6.QtGui import QFont from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout, - QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, - QPushButton, QScrollArea, QSizePolicy, - QStatusBar, QWidget, QSplitter, QCheckBox, - QSpacerItem) + QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow, + QPushButton, QScrollArea, QSizePolicy, + QStatusBar, QWidget, QSplitter, QCheckBox, + QSpacerItem) from src.qt.pagination import Pagination +from src.qt.widgets.landing import LandingWidget + +# Only import for type checking/autocompletion, will not be imported at runtime. +if typing.TYPE_CHECKING: + from src.qt.ts_qt import QtDriver + +logging.basicConfig(format="%(message)s", level=logging.INFO) class Ui_MainWindow(QMainWindow): - def __init__(self, parent=None) -> None: - super().__init__(parent) - self.setupUi(self) + def __init__(self, driver: "QtDriver", parent=None) -> None: + super().__init__(parent) + self.driver: "QtDriver" = driver + self.setupUi(self) - # self.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True) - # self.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False) - # # self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) - # self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + # NOTE: These are old attempts to allow for a translucent/acrylic + # window effect. This may be attempted again in the future. + # self.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True) + # self.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False) + # # self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True) + # self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - # self.windowFX = WindowEffect() - # self.windowFX.setAcrylicEffect(self.winId(), isEnableShadow=False) + # self.windowFX = WindowEffect() + # self.windowFX.setAcrylicEffect(self.winId(), isEnableShadow=False) - # # self.setStyleSheet( - # # 'background:#EE000000;' - # # ) - + # # self.setStyleSheet( + # # 'background:#EE000000;' + # # ) + - def setupUi(self, MainWindow): - if not MainWindow.objectName(): - MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(1300, 720) - - self.centralwidget = QWidget(MainWindow) - self.centralwidget.setObjectName(u"centralwidget") - self.gridLayout = QGridLayout(self.centralwidget) - self.gridLayout.setObjectName(u"gridLayout") - self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(1300, 720) + + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.gridLayout = QGridLayout(self.centralwidget) + self.gridLayout.setObjectName(u"gridLayout") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") - # ComboBox goup for search type and thumbnail size - self.horizontalLayout_3 = QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") + # ComboBox goup for search type and thumbnail size + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") - # left side spacer - spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) - self.horizontalLayout_3.addItem(spacerItem) + # left side spacer + spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + self.horizontalLayout_3.addItem(spacerItem) - # Search type selector - self.comboBox_2 = QComboBox(self.centralwidget) - self.comboBox_2.setMinimumSize(QSize(165, 0)) - self.comboBox_2.setObjectName("comboBox_2") - self.comboBox_2.addItem("") - self.comboBox_2.addItem("") - self.horizontalLayout_3.addWidget(self.comboBox_2) + # Search type selector + self.comboBox_2 = QComboBox(self.centralwidget) + self.comboBox_2.setMinimumSize(QSize(165, 0)) + self.comboBox_2.setObjectName("comboBox_2") + self.comboBox_2.addItem("") + self.comboBox_2.addItem("") + self.horizontalLayout_3.addWidget(self.comboBox_2) # Thumbnail Size placeholder - self.comboBox = QComboBox(self.centralwidget) - self.comboBox.setObjectName(u"comboBox") - sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - self.comboBox.sizePolicy().hasHeightForWidth()) - self.comboBox.setSizePolicy(sizePolicy) - self.comboBox.setMinimumWidth(128) - self.comboBox.setMaximumWidth(128) - self.horizontalLayout_3.addWidget(self.comboBox) - self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) + self.comboBox = QComboBox(self.centralwidget) + self.comboBox.setObjectName(u"comboBox") + sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.comboBox.sizePolicy().hasHeightForWidth()) + self.comboBox.setSizePolicy(sizePolicy) + self.comboBox.setMinimumWidth(128) + self.comboBox.setMaximumWidth(128) + self.horizontalLayout_3.addWidget(self.comboBox) + self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1) - self.splitter = QSplitter() - self.splitter.setObjectName(u"splitter") - self.splitter.setHandleWidth(12) + self.splitter = QSplitter() + self.splitter.setObjectName(u"splitter") + self.splitter.setHandleWidth(12) - self.frame_container = QWidget() - self.frame_layout = QVBoxLayout(self.frame_container) - self.frame_layout.setSpacing(0) + self.frame_container = QWidget() + self.frame_layout = QVBoxLayout(self.frame_container) + self.frame_layout.setSpacing(0) - self.scrollArea = QScrollArea() - self.scrollArea.setObjectName(u"scrollArea") - self.scrollArea.setFocusPolicy(Qt.WheelFocus) - self.scrollArea.setFrameShape(QFrame.NoFrame) - self.scrollArea.setFrameShadow(QFrame.Plain) - self.scrollArea.setWidgetResizable(True) - self.scrollAreaWidgetContents = QWidget() - self.scrollAreaWidgetContents.setObjectName( - u"scrollAreaWidgetContents") - self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590)) - self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents) - self.gridLayout_2.setSpacing(8) - self.gridLayout_2.setObjectName(u"gridLayout_2") - self.gridLayout_2.setContentsMargins(0, 0, 0, 8) - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.frame_layout.addWidget(self.scrollArea) + self.scrollArea = QScrollArea() + self.scrollArea.setObjectName(u"scrollArea") + self.scrollArea.setFocusPolicy(Qt.WheelFocus) + self.scrollArea.setFrameShape(QFrame.NoFrame) + self.scrollArea.setFrameShadow(QFrame.Plain) + self.scrollArea.setWidgetResizable(True) + self.scrollAreaWidgetContents = QWidget() + self.scrollAreaWidgetContents.setObjectName( + u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590)) + self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents) + self.gridLayout_2.setSpacing(8) + self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout_2.setContentsMargins(0, 0, 0, 8) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.frame_layout.addWidget(self.scrollArea) + + self.landing_widget: LandingWidget = LandingWidget(self.driver, self.devicePixelRatio()) + self.frame_layout.addWidget(self.landing_widget) - # self.page_bar_controls = QWidget() - # self.page_bar_controls.setStyleSheet('background:blue;') - # self.page_bar_controls.setMinimumHeight(32) + self.pagination = Pagination() + self.frame_layout.addWidget(self.pagination) - self.pagination = Pagination() - self.frame_layout.addWidget(self.pagination) + self.horizontalLayout.addWidget(self.splitter) + self.splitter.addWidget(self.frame_container) + self.splitter.setStretchFactor(0, 1) - # self.frame_layout.addWidget(self.page_bar_controls) - # self.frame_layout.addWidget(self.page_bar_controls) + self.gridLayout.addLayout(self.horizontalLayout, 10, 0, 1, 1) - self.horizontalLayout.addWidget(self.splitter) - self.splitter.addWidget(self.frame_container) - self.splitter.setStretchFactor(0, 1) + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize) + self.backButton = QPushButton(self.centralwidget) + self.backButton.setObjectName(u"backButton") + self.backButton.setMinimumSize(QSize(0, 32)) + self.backButton.setMaximumSize(QSize(32, 16777215)) + font = QFont() + font.setPointSize(14) + font.setBold(True) + self.backButton.setFont(font) - self.gridLayout.addLayout(self.horizontalLayout, 10, 0, 1, 1) + self.horizontalLayout_2.addWidget(self.backButton) - self.horizontalLayout_2 = QHBoxLayout() - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize) - self.backButton = QPushButton(self.centralwidget) - self.backButton.setObjectName(u"backButton") - self.backButton.setMinimumSize(QSize(0, 32)) - self.backButton.setMaximumSize(QSize(32, 16777215)) - font = QFont() - font.setPointSize(14) - font.setBold(True) - self.backButton.setFont(font) + self.forwardButton = QPushButton(self.centralwidget) + self.forwardButton.setObjectName(u"forwardButton") + self.forwardButton.setMinimumSize(QSize(0, 32)) + self.forwardButton.setMaximumSize(QSize(32, 16777215)) + font1 = QFont() + font1.setPointSize(14) + font1.setBold(True) + font1.setKerning(True) + self.forwardButton.setFont(font1) - self.horizontalLayout_2.addWidget(self.backButton) + self.horizontalLayout_2.addWidget(self.forwardButton) - self.forwardButton = QPushButton(self.centralwidget) - self.forwardButton.setObjectName(u"forwardButton") - self.forwardButton.setMinimumSize(QSize(0, 32)) - self.forwardButton.setMaximumSize(QSize(32, 16777215)) - font1 = QFont() - font1.setPointSize(14) - font1.setBold(True) - font1.setKerning(True) - self.forwardButton.setFont(font1) + self.searchField = QLineEdit(self.centralwidget) + self.searchField.setObjectName(u"searchField") + self.searchField.setMinimumSize(QSize(0, 32)) + font2 = QFont() + font2.setPointSize(11) + font2.setBold(False) + self.searchField.setFont(font2) - self.horizontalLayout_2.addWidget(self.forwardButton) + self.horizontalLayout_2.addWidget(self.searchField) - self.searchField = QLineEdit(self.centralwidget) - self.searchField.setObjectName(u"searchField") - self.searchField.setMinimumSize(QSize(0, 32)) - font2 = QFont() - font2.setPointSize(11) - font2.setBold(False) - self.searchField.setFont(font2) + self.searchButton = QPushButton(self.centralwidget) + self.searchButton.setObjectName(u"searchButton") + self.searchButton.setMinimumSize(QSize(0, 32)) + self.searchButton.setFont(font2) - self.horizontalLayout_2.addWidget(self.searchField) + self.horizontalLayout_2.addWidget(self.searchButton) + self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1) + self.gridLayout_2.setContentsMargins(6, 6, 6, 6) - self.searchButton = QPushButton(self.centralwidget) - self.searchButton.setObjectName(u"searchButton") - self.searchButton.setMinimumSize(QSize(0, 32)) - self.searchButton.setFont(font2) + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth( + self.statusbar.sizePolicy().hasHeightForWidth()) + self.statusbar.setSizePolicy(sizePolicy1) + MainWindow.setStatusBar(self.statusbar) - self.horizontalLayout_2.addWidget(self.searchButton) - self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1) + self.retranslateUi(MainWindow) - # self.comboBox = QComboBox(self.centralwidget) - # self.comboBox.setObjectName(u"comboBox") - # sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - # sizePolicy.setHorizontalStretch(0) - # sizePolicy.setVerticalStretch(0) - # sizePolicy.setHeightForWidth( - # self.comboBox.sizePolicy().hasHeightForWidth()) - # self.comboBox.setSizePolicy(sizePolicy) - # self.comboBox.setMinimumWidth(128) - # self.comboBox.setMaximumWidth(128) + QMetaObject.connectSlotsByName(MainWindow) + # setupUi - # self.gridLayout.addWidget(self.comboBox, 4, 0, 1, 1, Qt.AlignRight) - - self.gridLayout_2.setContentsMargins(6, 6, 6, 6) - - MainWindow.setCentralWidget(self.centralwidget) - # self.menubar = QMenuBar(MainWindow) - # self.menubar.setObjectName(u"menubar") - # self.menubar.setGeometry(QRect(0, 0, 1280, 22)) - # MainWindow.setMenuBar(self.menubar) - self.statusbar = QStatusBar(MainWindow) - self.statusbar.setObjectName(u"statusbar") - sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - sizePolicy1.setHorizontalStretch(0) - sizePolicy1.setVerticalStretch(0) - sizePolicy1.setHeightForWidth( - self.statusbar.sizePolicy().hasHeightForWidth()) - self.statusbar.setSizePolicy(sizePolicy1) - MainWindow.setStatusBar(self.statusbar) - - # menu_bar = self.menuBar() - # self.setMenuBar(menu_bar) - # self.gridLayout.addWidget(menu_bar, 4, 0, 1, 1, Qt.AlignRight) - # self.frame_layout.addWidget(menu_bar) - - self.retranslateUi(MainWindow) - - QMetaObject.connectSlotsByName(MainWindow) - # setupUi - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate( - "MainWindow", u"MainWindow", None)) + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate( + "MainWindow", u"MainWindow", None)) # Navigation buttons - self.backButton.setText( - QCoreApplication.translate("MainWindow", u"<", None)) - self.forwardButton.setText( - QCoreApplication.translate("MainWindow", u">", None)) + self.backButton.setText( + QCoreApplication.translate("MainWindow", u"<", None)) + self.forwardButton.setText( + QCoreApplication.translate("MainWindow", u">", None)) # Search field - self.searchField.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Search Entries", None)) - self.searchButton.setText( - QCoreApplication.translate("MainWindow", u"Search", None)) + self.searchField.setPlaceholderText( + QCoreApplication.translate("MainWindow", u"Search Entries", None)) + self.searchButton.setText( + QCoreApplication.translate("MainWindow", u"Search", None)) # Search type selector - self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)")) - self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)")) - self.comboBox.setCurrentText("") + self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)")) + self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)")) + self.comboBox.setCurrentText("") - # Tumbnail size selector - self.comboBox.setPlaceholderText( - QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) - # retranslateUi + # Thumbnail size selector + self.comboBox.setPlaceholderText( + QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) + # retranslateUi - def moveEvent(self, event) -> None: - # time.sleep(0.02) # sleep for 20ms - pass + def moveEvent(self, event) -> None: + # time.sleep(0.02) # sleep for 20ms + pass - def resizeEvent(self, event) -> None: - # time.sleep(0.02) # sleep for 20ms - pass + def resizeEvent(self, event) -> None: + # time.sleep(0.02) # sleep for 20ms + pass - # def _createMenuBar(self, main_window): - # menu_bar = QMenuBar(main_window) - # file_menu = QMenu('&File', main_window) - # edit_menu = QMenu('&Edit', main_window) - # tools_menu = QMenu('&Tools', main_window) - # macros_menu = QMenu('&Macros', main_window) - # help_menu = QMenu('&Help', main_window) - - # file_menu.addAction(QAction('&New Library', main_window)) - # file_menu.addAction(QAction('&Open Library', main_window)) - # file_menu.addAction(QAction('&Save Library', main_window)) - # file_menu.addAction(QAction('&Close Library', main_window)) - - # file_menu.addAction(QAction('&Refresh Directories', main_window)) - # file_menu.addAction(QAction('&Add New Files to Library', main_window)) - - # menu_bar.addMenu(file_menu) - # menu_bar.addMenu(edit_menu) - # menu_bar.addMenu(tools_menu) - # menu_bar.addMenu(macros_menu) - # menu_bar.addMenu(help_menu) - - # main_window.setMenuBar(menu_bar) + def toggle_landing_page(self, enabled: bool): + if enabled: + self.scrollArea.setHidden(True) + self.landing_widget.setHidden(False) + self.landing_widget.animate_logo_in() + else: + self.landing_widget.setHidden(True) + self.landing_widget.set_status_label("") + self.scrollArea.setHidden(False) \ No newline at end of file diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 5447182e..6da29000 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -21,7 +21,15 @@ from queue import Queue from typing import Optional from PIL import Image from PySide6 import QtCore -from PySide6.QtCore import QObject, QThread, Signal, Qt, QThreadPool, QTimer, QSettings +from PySide6.QtCore import ( + QObject, + QThread, + Signal, + Qt, + QThreadPool, + QTimer, + QSettings, +) from PySide6.QtGui import ( QGuiApplication, QPixmap, @@ -51,22 +59,6 @@ from src.core.enums import SettingItems, SearchMode from src.core.library import ItemType from src.core.ts_core import TagStudioCore from src.core.constants import ( - PLAINTEXT_TYPES, - TAG_COLORS, - DATE_FIELDS, - TEXT_FIELDS, - BOX_FIELDS, - ALL_FILE_TYPES, - SHORTCUT_TYPES, - PROGRAM_TYPES, - ARCHIVE_TYPES, - PRESENTATION_TYPES, - SPREADSHEET_TYPES, - DOC_TYPES, - AUDIO_TYPES, - VIDEO_TYPES, - IMAGE_TYPES, - LIBRARY_FILENAME, COLLAGE_FOLDER_NAME, BACKUP_FOLDER_NAME, TS_FOLDER_NAME, @@ -266,7 +258,7 @@ class QtDriver(QObject): timer.timeout.connect(lambda: None) # self.main_window = loader.load(home_path) - self.main_window = Ui_MainWindow() + self.main_window = Ui_MainWindow(self) self.main_window.setWindowTitle(self.base_title) self.main_window.mousePressEvent = self.mouse_navigation # type: ignore # self.main_window.setStyleSheet( @@ -557,6 +549,7 @@ class QtDriver(QObject): self.shutdown() def init_library_window(self): + # self._init_landing_page() # Taken care of inside the widget now self._init_thumb_grid() # TODO: Put this into its own method that copies the font file(s) into memory @@ -585,6 +578,13 @@ class QtDriver(QObject): forward_button: QPushButton = self.main_window.forwardButton forward_button.clicked.connect(self.nav_forward) + # NOTE: Putting this early will result in a white non-responsive + # window until everything is loaded. Consider adding a splash screen + # or implementing some clever loading tricks. + self.main_window.show() + self.main_window.activateWindow() + self.main_window.toggle_landing_page(True) + self.frame_dict = {} self.main_window.pagination.index.connect( lambda i: ( @@ -606,11 +606,6 @@ class QtDriver(QObject): # self.render_times: list = [] # self.main_window.setWindowFlag(Qt.FramelessWindowHint) - # NOTE: Putting this early will result in a white non-responsive - # window until everything is loaded. Consider adding a splash screen - # or implementing some clever loading tricks. - self.main_window.show() - self.main_window.activateWindow() # self.main_window.raise_() self.splash.finish(self.main_window) self.preview_panel.update_widgets() @@ -696,6 +691,7 @@ class QtDriver(QObject): self.selected.clear() self.preview_panel.update_widgets() self.filter_items() + self.main_window.toggle_landing_page(True) end_time = time.time() self.main_window.statusbar.showMessage( @@ -1439,15 +1435,18 @@ class QtDriver(QObject): def open_library(self, path: Path): """Opens a TagStudio library.""" + open_message: str = f'Opening Library "{str(path)}"...' + self.main_window.landing_widget.set_status_label(open_message) + self.main_window.statusbar.showMessage(open_message, 3) + self.main_window.repaint() + if self.lib.library_dir: self.save_library() self.lib.clear_internal_vars() - self.main_window.statusbar.showMessage(f"Opening Library {str(path)}", 3) return_code = self.lib.open_library(path) if return_code == 1: pass - else: logging.info( f"{ERROR} No existing TagStudio library found at '{path}'. Creating one." @@ -1465,6 +1464,7 @@ class QtDriver(QObject): self.selected.clear() self.preview_panel.update_widgets() self.filter_items() + self.main_window.toggle_landing_page(False) def create_collage(self) -> None: """Generates and saves an image collage based on Library Entries.""" diff --git a/tagstudio/src/qt/widgets/clickable_label.py b/tagstudio/src/qt/widgets/clickable_label.py new file mode 100644 index 00000000..ca812f95 --- /dev/null +++ b/tagstudio/src/qt/widgets/clickable_label.py @@ -0,0 +1,18 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +from PySide6.QtCore import Signal +from PySide6.QtWidgets import QLabel + + +class ClickableLabel(QLabel): + """A clickable Label widget.""" + + clicked = Signal() + + def __init__(self): + super().__init__() + + def mousePressEvent(self, event): + self.clicked.emit() diff --git a/tagstudio/src/qt/widgets/landing.py b/tagstudio/src/qt/widgets/landing.py new file mode 100644 index 00000000..9df3fa4d --- /dev/null +++ b/tagstudio/src/qt/widgets/landing.py @@ -0,0 +1,181 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + + +import logging +import sys +import typing +from pathlib import Path +from PIL import Image, ImageQt +from PySide6.QtCore import Qt, QPropertyAnimation, QPoint, QEasingCurve +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QPushButton +from src.qt.widgets.clickable_label import ClickableLabel +from src.qt.helpers.color_overlay import gradient_overlay, theme_fg_overlay + +# Only import for type checking/autocompletion, will not be imported at runtime. +if typing.TYPE_CHECKING: + from src.qt.ts_qt import QtDriver + +logging.basicConfig(format="%(message)s", level=logging.INFO) + + +class LandingWidget(QWidget): + def __init__(self, driver: "QtDriver", pixel_ratio: float): + super().__init__() + self.driver: "QtDriver" = driver + self.logo_label: ClickableLabel = ClickableLabel() + self._pixel_ratio: float = pixel_ratio + self._logo_width: int = int(480 * pixel_ratio) + self._special_click_count: int = 0 + + # Create layout -------------------------------------------------------- + self.landing_layout = QVBoxLayout() + self.landing_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.landing_layout.setSpacing(12) + self.setLayout(self.landing_layout) + + # Create landing logo -------------------------------------------------- + # self.landing_logo_pixmap = QPixmap(":/images/tagstudio_logo_text_mono.png") + self.logo_raw: Image.Image = Image.open( + Path(__file__).parents[3] + / "resources/qt/images/tagstudio_logo_text_mono.png" + ) + self.landing_pixmap: QPixmap = QPixmap() + self.update_logo_color() + self.logo_label.clicked.connect(self._update_special_click) + + # Initialize landing logo animation ------------------------------------ + self.logo_pos_anim = QPropertyAnimation(self.logo_label, b"pos") + self.logo_pos_anim.setEasingCurve(QEasingCurve.Type.OutCubic) + self.logo_pos_anim.setDuration(1000) + + self.logo_special_anim = QPropertyAnimation(self.logo_label, b"pos") + self.logo_special_anim.setEasingCurve(QEasingCurve.Type.OutCubic) + self.logo_special_anim.setDuration(500) + + # Create "Open/Create Library" button ---------------------------------- + open_shortcut_text: str = "" + if sys.platform == "darwin": + open_shortcut_text = "(⌘+O)" + else: + open_shortcut_text = "(Ctrl+O)" + self.open_button: QPushButton = QPushButton() + self.open_button.setMinimumWidth(200) + self.open_button.setText(f"Open/Create Library {open_shortcut_text}") + self.open_button.clicked.connect(self.driver.open_library_from_dialog) + + # Create status label -------------------------------------------------- + self.status_label = QLabel() + self.status_label.setMinimumWidth(200) + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.status_label.setText("") + + # Initialize landing logo animation ------------------------------------ + self.status_pos_anim = QPropertyAnimation(self.status_label, b"pos") + self.status_pos_anim.setEasingCurve(QEasingCurve.Type.OutCubic) + self.status_pos_anim.setDuration(500) + + # Add widgets to layout ------------------------------------------------ + self.landing_layout.addWidget(self.logo_label) + self.landing_layout.addWidget( + self.open_button, alignment=Qt.AlignmentFlag.AlignCenter + ) + self.landing_layout.addWidget( + self.status_label, alignment=Qt.AlignmentFlag.AlignCenter + ) + + def update_logo_color(self, style: str = "mono"): + """ + Update the color of the TagStudio logo. + + Args: + style (str): = The style of the logo. Either "mono" or "gradient". + """ + + logo_im: Image.Image = None + if style == "mono": + logo_im = theme_fg_overlay(self.logo_raw) + elif style == "gradient": + gradient_colors: list[str] = ["#d27bf4", "#7992f5", "#63c6e3", "#63f5cf"] + logo_im = gradient_overlay(self.logo_raw, gradient_colors) + + logo_final: Image.Image = Image.new( + mode="RGBA", size=self.logo_raw.size, color="#00000000" + ) + + logo_final.paste(logo_im, (0, 0), mask=self.logo_raw) + + self.landing_pixmap = QPixmap.fromImage(ImageQt.ImageQt(logo_im)) + self.landing_pixmap.setDevicePixelRatio(self._pixel_ratio) + self.landing_pixmap = self.landing_pixmap.scaledToWidth( + self._logo_width, Qt.TransformationMode.SmoothTransformation + ) + self.logo_label.setMaximumHeight( + int(self.logo_raw.size[1] * (self.logo_raw.size[0] / self._logo_width)) + ) + self.logo_label.setMaximumWidth(self._logo_width) + self.logo_label.setPixmap(self.landing_pixmap) + + def _update_special_click(self): + """ + Increment the click count for the logo easter egg if it has not + been triggered. If it reaches the click threshold, this triggers it + and prevents it from triggering again. + """ + if self._special_click_count >= 0: + self._special_click_count += 1 + if self._special_click_count >= 10: + self.update_logo_color("gradient") + self.animate_logo_pop() + self._special_click_count = -1 + + def animate_logo_in(self): + """Animate in the TagStudio logo.""" + # NOTE: Sometimes, mostly on startup without a library open, the + # y position of logo_label is something like 10. I'm not sure what + # the cause of this is, so I've just done this workaround to disable + # the animation if the y position is too incorrect. + if self.logo_label.y() > 50: + self.logo_pos_anim.setStartValue( + QPoint(self.logo_label.x(), self.logo_label.y() - 100) + ) + self.logo_pos_anim.setEndValue(self.logo_label.pos()) + self.logo_pos_anim.start() + + def animate_logo_pop(self): + """Special pop animation for the TagStudio logo.""" + self.logo_special_anim.setStartValue(self.logo_label.pos()) + self.logo_special_anim.setKeyValueAt( + 0.25, QPoint(self.logo_label.x() - 5, self.logo_label.y()) + ) + self.logo_special_anim.setKeyValueAt( + 0.5, QPoint(self.logo_label.x() + 5, self.logo_label.y() - 10) + ) + self.logo_special_anim.setKeyValueAt( + 0.75, QPoint(self.logo_label.x() - 5, self.logo_label.y()) + ) + self.logo_special_anim.setEndValue(self.logo_label.pos()) + + self.logo_special_anim.start() + + # def animate_status(self): + # # if self.status_label.y() > 50: + # logging.info(f"{self.status_label.pos()}") + # self.status_pos_anim.setStartValue( + # QPoint(self.status_label.x(), self.status_label.y() + 50) + # ) + # self.status_pos_anim.setEndValue(self.status_label.pos()) + # self.status_pos_anim.start() + + def set_status_label(self, text=str): + """ + Set the text of the status label. + + Args: + text (str): Text of the status to set. + """ + # if text: + # self.animate_status() + self.status_label.setText(text)