mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-31 15:19:10 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
50
.github/workflows/apprun.yaml
vendored
Normal file
50
.github/workflows/apprun.yaml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: PySide App Test
|
||||
|
||||
on: [ push, pull_request ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
# dont run update, it is slow
|
||||
# sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libxkbcommon-x11-0 \
|
||||
x11-utils \
|
||||
libyaml-dev \
|
||||
libegl1-mesa \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-randr0 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-xinerama0 \
|
||||
libopengl0 \
|
||||
libxcb-cursor0
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run TagStudio app and check exit code
|
||||
run: |
|
||||
xvfb-run --server-args="-screen 0, 1920x1200x24 -ac +extension GLX +render -noreset" python tagstudio/tag_studio.py --ci -o /tmp/
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "TagStudio ran successfully"
|
||||
else
|
||||
echo "TagStudio failed with exit code $exit_code"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,2 +1,3 @@
|
||||
ruff==0.4.2
|
||||
pre-commit==3.7.0
|
||||
Pyinstaller==6.6.0
|
||||
|
||||
@@ -7,4 +7,3 @@ PySide6_Addons>=6.5.1.1,<=6.6.3.1
|
||||
PySide6_Essentials>=6.5.1.1,<=6.6.3.1
|
||||
typing_extensions>=3.10.0.0,<=4.11.0
|
||||
ujson>=5.8.0,<=5.9.0
|
||||
Pyinstaller==6.6.0
|
||||
|
||||
Binary file not shown.
@@ -12,6 +12,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
from PIL import Image, ImageOps, ImageChops, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
import pillow_avif
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
@@ -643,8 +644,12 @@ class CliDriver:
|
||||
# raw.thumbnail((512, 512))
|
||||
raw.thumbnail(self.external_preview_size)
|
||||
raw.save(external_preview_path)
|
||||
except:
|
||||
print(f'{ERROR} Could not load image "{filepath}"')
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
FileNotFoundError,
|
||||
DecompressionBombError,
|
||||
) as e:
|
||||
print(f'{ERROR} Could not load image "{filepath} due to {e}"')
|
||||
if self.args.external_preview:
|
||||
self.set_external_preview_broken()
|
||||
elif file_type in VIDEO_TYPES:
|
||||
@@ -1109,24 +1114,34 @@ class CliDriver:
|
||||
# sys.stdout.write(f'\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}')
|
||||
# sys.stdout.flush()
|
||||
if file_type in IMAGE_TYPES:
|
||||
with Image.open(
|
||||
os.path.normpath(
|
||||
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
|
||||
)
|
||||
) as pic:
|
||||
if keep_aspect:
|
||||
pic.thumbnail((thumb_size, thumb_size))
|
||||
else:
|
||||
pic = pic.resize((thumb_size, thumb_size))
|
||||
if data_tint_mode and color:
|
||||
pic = pic.convert(mode="RGB")
|
||||
pic = ImageChops.hard_light(
|
||||
pic,
|
||||
Image.new(
|
||||
"RGB", (thumb_size, thumb_size), color
|
||||
),
|
||||
try:
|
||||
with Image.open(
|
||||
os.path.normpath(
|
||||
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
|
||||
)
|
||||
collage.paste(pic, (y * thumb_size, x * thumb_size))
|
||||
) as pic:
|
||||
if keep_aspect:
|
||||
pic.thumbnail((thumb_size, thumb_size))
|
||||
else:
|
||||
pic = pic.resize((thumb_size, thumb_size))
|
||||
if data_tint_mode and color:
|
||||
pic = pic.convert(mode="RGB")
|
||||
pic = ImageChops.hard_light(
|
||||
pic,
|
||||
Image.new(
|
||||
"RGB",
|
||||
(thumb_size, thumb_size),
|
||||
color,
|
||||
),
|
||||
)
|
||||
collage.paste(
|
||||
pic, (y * thumb_size, x * thumb_size)
|
||||
)
|
||||
except DecompressionBombError as e:
|
||||
print(
|
||||
f"[ERROR] One of the images was too big ({e})"
|
||||
)
|
||||
|
||||
elif file_type in VIDEO_TYPES:
|
||||
video = cv2.VideoCapture(filepath)
|
||||
video.set(
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .file_opener import open_file, FileOpenerHelper, FileOpenerLabel
|
||||
from .function_iterator import FunctionIterator
|
||||
from .custom_runnable import CustomRunnable
|
||||
@@ -17,8 +17,8 @@ from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,
|
||||
from PySide6.QtGui import (QFont, QAction)
|
||||
from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout,
|
||||
QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow,
|
||||
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
|
||||
QStatusBar, QWidget, QSplitter, QMenu)
|
||||
QPushButton, QScrollArea, QSizePolicy,
|
||||
QStatusBar, QWidget, QSplitter)
|
||||
from src.qt.pagination import Pagination
|
||||
|
||||
|
||||
@@ -167,10 +167,10 @@ class Ui_MainWindow(QMainWindow):
|
||||
self.statusbar.setSizePolicy(sizePolicy1)
|
||||
MainWindow.setStatusBar(self.statusbar)
|
||||
|
||||
menu_bar = self.menuBar()
|
||||
self.setMenuBar(menu_bar)
|
||||
# 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.frame_layout.addWidget(menu_bar)
|
||||
|
||||
self.retranslateUi(MainWindow)
|
||||
|
||||
@@ -201,26 +201,26 @@ class Ui_MainWindow(QMainWindow):
|
||||
# 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)
|
||||
# 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('&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))
|
||||
# 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)
|
||||
# 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)
|
||||
# main_window.setMenuBar(menu_bar)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from .tag_search import TagSearchPanel
|
||||
from .build_tag import BuildTagPanel
|
||||
from .tag_database import TagDatabasePanel
|
||||
from .add_field import AddFieldModal
|
||||
from .file_extension import FileExtensionModal
|
||||
from .delete_unlinked import DeleteUnlinkedEntriesModal
|
||||
from .relink_unlinked import RelinkUnlinkedEntries
|
||||
from .fix_unlinked import FixUnlinkedEntriesModal
|
||||
from .mirror_entities import MirrorEntriesModal
|
||||
from .fix_dupes import FixDupeFilesModal
|
||||
from .folders_to_tags import FoldersToTagsModal
|
||||
@@ -21,8 +21,9 @@ from PySide6.QtWidgets import (
|
||||
from src.core.library import Library, Tag
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.core.ts_core import TAG_COLORS
|
||||
from src.qt.widgets import PanelWidget, PanelModal, TagWidget
|
||||
from src.qt.modals import TagSearchPanel
|
||||
from src.qt.widgets.panel import PanelWidget, PanelModal
|
||||
from src.qt.widgets.tag import TagWidget
|
||||
from src.qt.modals.tag_search import TagSearchPanel
|
||||
|
||||
|
||||
ERROR = f"[ERROR]"
|
||||
|
||||
@@ -16,8 +16,9 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from src.core.library import ItemType, Library
|
||||
from src.qt.helpers import CustomRunnable, FunctionIterator
|
||||
from src.qt.widgets import ProgressWidget
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
@@ -7,7 +7,7 @@ from PySide6.QtCore import Signal, Qt
|
||||
from PySide6.QtWidgets import QVBoxLayout, QPushButton, QTableWidget, QTableWidgetItem
|
||||
|
||||
from src.core.library import Library
|
||||
from src.qt.widgets import PanelWidget
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
|
||||
|
||||
class FileExtensionModal(PanelWidget):
|
||||
|
||||
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from src.core.library import Library
|
||||
from src.qt.modals import MirrorEntriesModal
|
||||
from src.qt.modals.mirror_entities import MirrorEntriesModal
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
@@ -10,9 +10,11 @@ from PySide6.QtCore import QThread, Qt, QThreadPool
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
|
||||
from src.core.library import Library
|
||||
from src.qt.helpers import FunctionIterator, CustomRunnable
|
||||
from src.qt.modals import DeleteUnlinkedEntriesModal, RelinkUnlinkedEntries
|
||||
from src.qt.widgets import ProgressWidget
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal
|
||||
from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
@@ -18,8 +18,9 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from src.core.library import Library
|
||||
from src.qt.helpers import FunctionIterator, CustomRunnable
|
||||
from src.qt.widgets import ProgressWidget
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
@@ -7,8 +7,9 @@ import typing
|
||||
from PySide6.QtCore import QObject, Signal, QThreadPool
|
||||
|
||||
from src.core.library import Library
|
||||
from src.qt.helpers import FunctionIterator, CustomRunnable
|
||||
from src.qt.widgets import ProgressWidget
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
@@ -13,8 +13,9 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from src.core.library import Library
|
||||
from src.qt.widgets import PanelWidget, PanelModal, TagWidget
|
||||
from src.qt.modals import BuildTagPanel
|
||||
from src.qt.widgets.panel import PanelWidget, PanelModal
|
||||
from src.qt.widgets.tag import TagWidget
|
||||
from src.qt.modals.build_tag import BuildTagPanel
|
||||
|
||||
|
||||
class TagDatabasePanel(PanelWidget):
|
||||
|
||||
@@ -19,7 +19,8 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from src.core.library import Library
|
||||
from src.core.palette import ColorType, get_tag_color
|
||||
from src.qt.widgets import PanelWidget, TagWidget
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
from src.qt.widgets.tag import TagWidget
|
||||
|
||||
|
||||
ERROR = f"[ERROR]"
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
<!-- <file alias = "images/edit_icon_128.png">../../resources/qt/images/edit_icon_128.png</file> -->
|
||||
<!-- <file alias = "images/trash_icon_128.png">../../resources/qt/images/trash_icon_128.png</file> -->
|
||||
<!-- <file alias = "images/clipboard_icon_128.png">../../resources/qt/images/clipboard_icon_128.png</file> -->
|
||||
<!-- <file alias = "images/splash.png">../../resources/qt/images/splash.png</file> -->
|
||||
<file alias = "images/splash.png">../../resources/qt/images/splash.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ from PySide6.QtWidgets import (
|
||||
QFileDialog,
|
||||
QSplashScreen,
|
||||
QMenu,
|
||||
QMenuBar,
|
||||
)
|
||||
from humanfriendly import format_timespan
|
||||
|
||||
@@ -73,24 +74,20 @@ from src.core.ts_core import (
|
||||
from src.core.utils.web import strip_web_protocol
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.main_window import Ui_MainWindow
|
||||
from src.qt.helpers import FunctionIterator, CustomRunnable
|
||||
from src.qt.widgets import (
|
||||
CollageIconRenderer,
|
||||
ThumbRenderer,
|
||||
PanelModal,
|
||||
ProgressWidget,
|
||||
PreviewPanel,
|
||||
ItemThumb,
|
||||
)
|
||||
from src.qt.modals import (
|
||||
BuildTagPanel,
|
||||
TagDatabasePanel,
|
||||
FileExtensionModal,
|
||||
FixUnlinkedEntriesModal,
|
||||
FixDupeFilesModal,
|
||||
FoldersToTagsModal,
|
||||
)
|
||||
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.widgets.collage_icon import CollageIconRenderer
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
from src.qt.widgets.preview_panel import PreviewPanel
|
||||
from src.qt.widgets.item_thumb import ItemThumb
|
||||
from src.qt.modals.build_tag import BuildTagPanel
|
||||
from src.qt.modals.tag_database import TagDatabasePanel
|
||||
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
|
||||
|
||||
# SIGQUIT is not defined on Windows
|
||||
@@ -203,6 +200,9 @@ class QtDriver(QObject):
|
||||
)
|
||||
|
||||
max_threads = os.cpu_count()
|
||||
if args.ci:
|
||||
# spawn only single worker in CI environment
|
||||
max_threads = 1
|
||||
for i in range(max_threads):
|
||||
# thread = threading.Thread(target=self.consumer, name=f'ThumbRenderer_{i}',args=(), daemon=True)
|
||||
# thread.start()
|
||||
@@ -246,6 +246,7 @@ class QtDriver(QObject):
|
||||
|
||||
# Handle OS signals
|
||||
self.setup_signals()
|
||||
# allow to process input from console, eg. SIGTERM
|
||||
timer = QTimer()
|
||||
timer.start(500)
|
||||
timer.timeout.connect(lambda: None)
|
||||
@@ -267,19 +268,24 @@ class QtDriver(QObject):
|
||||
# self.windowFX = WindowEffect()
|
||||
# self.windowFX.setAcrylicEffect(self.main_window.winId())
|
||||
|
||||
splash_pixmap = QPixmap(
|
||||
f"{Path(__file__).parents[2]}/resources/qt/images/splash.png"
|
||||
)
|
||||
splash_pixmap = QPixmap(":/images/splash.png")
|
||||
splash_pixmap.setDevicePixelRatio(self.main_window.devicePixelRatio())
|
||||
self.splash = QSplashScreen(splash_pixmap)
|
||||
self.splash = QSplashScreen(splash_pixmap, Qt.WindowStaysOnTopHint)
|
||||
# self.splash.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.splash.show()
|
||||
|
||||
menu_bar = self.main_window.menuBar()
|
||||
if os.name == "nt":
|
||||
appid = "cyanvoxel.tagstudio.9"
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
|
||||
|
||||
# Allow the use of the native macOS menu bar.
|
||||
# if sys.platform != "darwin":
|
||||
menu_bar.setNativeMenuBar(False)
|
||||
if sys.platform != "darwin":
|
||||
icon = QIcon()
|
||||
icon.addFile(icon_path)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
menu_bar = QMenuBar(self.main_window)
|
||||
self.main_window.setMenuBar(menu_bar)
|
||||
menu_bar.setNativeMenuBar(True)
|
||||
|
||||
file_menu = QMenu("&File", menu_bar)
|
||||
edit_menu = QMenu("&Edit", menu_bar)
|
||||
@@ -445,26 +451,9 @@ class QtDriver(QObject):
|
||||
menu_bar.addMenu(macros_menu)
|
||||
menu_bar.addMenu(help_menu)
|
||||
|
||||
# self.main_window.setMenuBar(menu_bar)
|
||||
# self.main_window.centralWidget().layout().addWidget(menu_bar, 0,0,1,1)
|
||||
# self.main_window.tb_layout.addWidget(menu_bar)
|
||||
|
||||
if sys.platform != "darwin":
|
||||
icon = QIcon()
|
||||
icon.addFile(icon_path)
|
||||
self.main_window.setWindowIcon(icon)
|
||||
|
||||
self.preview_panel = PreviewPanel(self.lib, self)
|
||||
l: QHBoxLayout = self.main_window.splitter
|
||||
l.addWidget(self.preview_panel)
|
||||
# self.preview_panel.update_widgets()
|
||||
# l.setEnabled(False)
|
||||
# self.entry_panel.setWindowIcon(icon)
|
||||
|
||||
if os.name == "nt":
|
||||
appid = "cyanvoxel.tagstudio.9"
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
QFontDatabase.addApplicationFont(
|
||||
os.path.normpath(
|
||||
@@ -477,7 +466,6 @@ class QtDriver(QObject):
|
||||
self.item_thumbs: list[ItemThumb] = []
|
||||
self.thumb_renderers: list[ThumbRenderer] = []
|
||||
self.collation_thumb_size = math.ceil(self.thumb_size * 2)
|
||||
# self.filtered_items: list[tuple[SearchItemType, int]] = []
|
||||
|
||||
self._init_thumb_grid()
|
||||
|
||||
@@ -549,7 +537,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()
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from .fields import FieldContainer, FieldWidget
|
||||
from .collage_icon import CollageIconRenderer
|
||||
from .thumb_button import ThumbButton
|
||||
from .thumb_renderer import ThumbRenderer
|
||||
from .panel import PanelWidget, PanelModal
|
||||
from .text_box_edit import EditTextBox
|
||||
from .text_line_edit import EditTextLine
|
||||
from .progress import ProgressWidget
|
||||
from .tag import TagWidget
|
||||
from .tag_box import TagBoxWidget
|
||||
from .text import TextWidget
|
||||
from .item_thumb import ItemThumb
|
||||
from .preview_panel import PreviewPanel
|
||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
|
||||
import cv2
|
||||
from PIL import Image, ImageChops, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import (
|
||||
QObject,
|
||||
QThread,
|
||||
@@ -95,22 +96,25 @@ class CollageIconRenderer(QObject):
|
||||
# sys.stdout.write(f'\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}')
|
||||
# sys.stdout.flush()
|
||||
if file_type in IMAGE_TYPES:
|
||||
with Image.open(
|
||||
os.path.normpath(
|
||||
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
|
||||
)
|
||||
) as pic:
|
||||
if keep_aspect:
|
||||
pic.thumbnail(size)
|
||||
else:
|
||||
pic = pic.resize(size)
|
||||
if data_tint_mode and color:
|
||||
pic = pic.convert(mode="RGB")
|
||||
pic = ImageChops.hard_light(
|
||||
pic, Image.new("RGB", size, color)
|
||||
try:
|
||||
with Image.open(
|
||||
os.path.normpath(
|
||||
f"{self.lib.library_dir}/{entry.path}/{entry.filename}"
|
||||
)
|
||||
# collage.paste(pic, (y*thumb_size, x*thumb_size))
|
||||
self.rendered.emit(pic)
|
||||
) as pic:
|
||||
if keep_aspect:
|
||||
pic.thumbnail(size)
|
||||
else:
|
||||
pic = pic.resize(size)
|
||||
if data_tint_mode and color:
|
||||
pic = pic.convert(mode="RGB")
|
||||
pic = ImageChops.hard_light(
|
||||
pic, Image.new("RGB", size, color)
|
||||
)
|
||||
# collage.paste(pic, (y*thumb_size, x*thumb_size))
|
||||
self.rendered.emit(pic)
|
||||
except DecompressionBombError as e:
|
||||
logging.info(f"[ERROR] One of the images was too big ({e})")
|
||||
elif file_type in VIDEO_TYPES:
|
||||
video = cv2.VideoCapture(filepath)
|
||||
video.set(
|
||||
|
||||
@@ -26,11 +26,12 @@ from PySide6.QtWidgets import (
|
||||
from src.core.library import ItemType, Library, Entry
|
||||
from src.core.ts_core import AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES
|
||||
from src.qt.flowlayout import FlowWidget
|
||||
from src.qt.helpers import FileOpenerHelper
|
||||
from src.qt.widgets import ThumbRenderer, ThumbButton
|
||||
from src.qt.helpers.file_opener import FileOpenerHelper
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
from src.qt.widgets.thumb_button import ThumbButton
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from src.qt.widgets import PreviewPanel
|
||||
from src.qt.widgets.preview_panel import PreviewPanel
|
||||
|
||||
ERROR = f"[ERROR]"
|
||||
WARNING = f"[WARNING]"
|
||||
|
||||
@@ -11,6 +11,7 @@ from datetime import datetime as dt
|
||||
|
||||
import cv2
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import Signal, Qt, QSize
|
||||
from PySide6.QtGui import QResizeEvent, QAction
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -29,18 +30,16 @@ from humanfriendly import format_size
|
||||
|
||||
from src.core.library import Entry, ItemType, Library
|
||||
from src.core.ts_core import VIDEO_TYPES, IMAGE_TYPES
|
||||
from src.qt.helpers import FileOpenerLabel, FileOpenerHelper, open_file
|
||||
from src.qt.modals import AddFieldModal
|
||||
from src.qt.widgets import (
|
||||
ThumbRenderer,
|
||||
FieldContainer,
|
||||
TagBoxWidget,
|
||||
TextWidget,
|
||||
PanelModal,
|
||||
EditTextBox,
|
||||
EditTextLine,
|
||||
ItemThumb,
|
||||
)
|
||||
from src.qt.helpers.file_opener import FileOpenerLabel, FileOpenerHelper, open_file
|
||||
from src.qt.modals.add_field import AddFieldModal
|
||||
from src.qt.widgets.thumb_renderer import ThumbRenderer
|
||||
from src.qt.widgets.fields import FieldContainer
|
||||
from src.qt.widgets.tag_box import TagBoxWidget
|
||||
from src.qt.widgets.text import TextWidget
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
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:
|
||||
@@ -405,8 +404,15 @@ class PreviewPanel(QWidget):
|
||||
)
|
||||
raise UnidentifiedImageError
|
||||
|
||||
except (UnidentifiedImageError, FileNotFoundError, cv2.error):
|
||||
pass
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
FileNotFoundError,
|
||||
cv2.error,
|
||||
DecompressionBombError,
|
||||
) as e:
|
||||
logging.info(
|
||||
f"[PreviewPanel][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})"
|
||||
)
|
||||
|
||||
try:
|
||||
self.preview_img.clicked.disconnect()
|
||||
|
||||
@@ -12,8 +12,11 @@ from PySide6.QtWidgets import QPushButton
|
||||
|
||||
from src.core.library import Library, Tag
|
||||
from src.qt.flowlayout import FlowLayout
|
||||
from src.qt.widgets import FieldWidget, TagWidget, PanelModal
|
||||
from src.qt.modals import BuildTagPanel, TagSearchPanel
|
||||
from src.qt.widgets.fields import FieldWidget
|
||||
from src.qt.widgets.tag import TagWidget
|
||||
from src.qt.widgets.panel import PanelModal
|
||||
from src.qt.modals.build_tag import BuildTagPanel
|
||||
from src.qt.modals.tag_search import TagSearchPanel
|
||||
|
||||
# Only import for type checking/autocompletion, will not be imported at runtime.
|
||||
if typing.TYPE_CHECKING:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel
|
||||
from src.qt.widgets import FieldWidget
|
||||
from src.qt.widgets.fields import FieldWidget
|
||||
|
||||
|
||||
class TextWidget(FieldWidget):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
from PySide6.QtWidgets import QVBoxLayout, QPlainTextEdit
|
||||
|
||||
from src.qt.widgets import PanelWidget
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
|
||||
|
||||
class EditTextBox(PanelWidget):
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Callable
|
||||
|
||||
from PySide6.QtWidgets import QVBoxLayout, QLineEdit
|
||||
|
||||
from src.qt.widgets import PanelWidget
|
||||
from src.qt.widgets.panel import PanelWidget
|
||||
|
||||
|
||||
class EditTextLine(PanelWidget):
|
||||
|
||||
@@ -21,6 +21,7 @@ from PIL import (
|
||||
ImageOps,
|
||||
ImageFile,
|
||||
)
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import QObject, Signal, QSize
|
||||
from PySide6.QtGui import QPixmap
|
||||
from src.core.ts_core import PLAINTEXT_TYPES, VIDEO_TYPES, IMAGE_TYPES
|
||||
@@ -138,17 +139,22 @@ class ThumbRenderer(QObject):
|
||||
try:
|
||||
# Images =======================================================
|
||||
if extension in IMAGE_TYPES:
|
||||
image = Image.open(filepath)
|
||||
# image = self.thumb_debug
|
||||
if image.mode == "RGBA":
|
||||
# logging.info(image.getchannel(3).tobytes())
|
||||
new_bg = Image.new("RGB", image.size, color="#1e1e1e")
|
||||
new_bg.paste(image, mask=image.getchannel(3))
|
||||
image = new_bg
|
||||
if image.mode != "RGB":
|
||||
image = image.convert(mode="RGB")
|
||||
try:
|
||||
image = Image.open(filepath)
|
||||
# image = self.thumb_debug
|
||||
if image.mode == "RGBA":
|
||||
# logging.info(image.getchannel(3).tobytes())
|
||||
new_bg = Image.new("RGB", image.size, color="#1e1e1e")
|
||||
new_bg.paste(image, mask=image.getchannel(3))
|
||||
image = new_bg
|
||||
if image.mode != "RGB":
|
||||
image = image.convert(mode="RGB")
|
||||
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
except DecompressionBombError as e:
|
||||
logging.info(
|
||||
f"[ThumbRenderer][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})"
|
||||
)
|
||||
|
||||
# Videos =======================================================
|
||||
elif extension in VIDEO_TYPES:
|
||||
@@ -321,17 +327,22 @@ class ThumbRenderer(QObject):
|
||||
try:
|
||||
# Images =======================================================
|
||||
if extension in IMAGE_TYPES:
|
||||
image = Image.open(filepath)
|
||||
# image = self.thumb_debug
|
||||
if image.mode == "RGBA":
|
||||
# logging.info(image.getchannel(3).tobytes())
|
||||
new_bg = Image.new("RGB", image.size, color="#1e1e1e")
|
||||
new_bg.paste(image, mask=image.getchannel(3))
|
||||
image = new_bg
|
||||
if image.mode != "RGB":
|
||||
image = image.convert(mode="RGB")
|
||||
try:
|
||||
image = Image.open(filepath)
|
||||
# image = self.thumb_debug
|
||||
if image.mode == "RGBA":
|
||||
# logging.info(image.getchannel(3).tobytes())
|
||||
new_bg = Image.new("RGB", image.size, color="#1e1e1e")
|
||||
new_bg.paste(image, mask=image.getchannel(3))
|
||||
image = new_bg
|
||||
if image.mode != "RGB":
|
||||
image = image.convert(mode="RGB")
|
||||
|
||||
image = ImageOps.exif_transpose(image)
|
||||
image = ImageOps.exif_transpose(image)
|
||||
except DecompressionBombError as e:
|
||||
logging.info(
|
||||
f"[ThumbRenderer][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})"
|
||||
)
|
||||
|
||||
# Videos =======================================================
|
||||
elif extension in VIDEO_TYPES:
|
||||
|
||||
@@ -53,6 +53,11 @@ def main():
|
||||
type=str,
|
||||
help="User interface option for TagStudio. Options: qt, cli (Default: qt)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ci",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Exit the application after checking it starts without any problem. Meant for CI check.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
core = TagStudioCore() # The TagStudio Core instance. UI agnostic.
|
||||
|
||||
Reference in New Issue
Block a user