mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-17 23:16:24 +00:00
* feat: Drag and drop files in and out of TagStudio (#153) * Ability to drop local files in to TagStudio to add to library * Added renaming option to drop import * Improved readability and switched to pathLib * format * Apply suggestions from code review Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com> * Revert Change * Update tagstudio/src/qt/modals/drop_import.py Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com> * Added support for folders * formatting * Progress bars added * Added Ability to Drag out of window * f * format * Ability to drop local files in to TagStudio to add to library * Added renaming option to drop import * Improved readability and switched to pathLib * format * Apply suggestions from code review Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com> * Revert Change * Update tagstudio/src/qt/modals/drop_import.py Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com> * Added support for folders * formatting * Progress bars added * Added Ability to Drag out of window * f * format * format * formatting and refactor * format again * formatting for mypy * convert lambda to func for clarity * mypy fixes * fixed dragout only worked on selected * Refactor typo, Add license * Reformat QMessageBox * Disable drops when no library is open Co-authored-by: Sean Krueger <skrueger2270@gmail.com> * Rebased onto SQL migration * Updated logic to based on selected grid_idx instead of selected ids * Add newly dragged-in files to SQL database * Fix buttons being inconsistant across platforms * Fix ruff formatting * Rename "override" button to "overwrite" --------- Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com> Co-authored-by: Travis Abendshien <lvnvtravis@gmail.com> Co-authored-by: Sean Krueger <skrueger2270@gmail.com> * refactor: Update dialog and simplify drop import logic * Handle Qt events for main window in ts_qt.py * Replace magic values with enums * Match import duplicate file dialog to delete missing entry dialog * Remove excessive progess widgets * Add docstrings and logging * refactor: add function for common ProgressWidget use Extracts the create_progress_bar function from drop_import to the ProgressWidget class. Instead of creating a ProgressWidget, FunctionIterator, and CustomRunnable every time a thread-safe progress widget is needed, the from_iterable function in ProgressWidget now handles all of that. --------- Co-authored-by: Creepler13 <denis.schlichting03@gmail.com> Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com> Co-authored-by: Travis Abendshien <lvnvtravis@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
import typing
|
||||
|
||||
from PySide6.QtCore import Qt, QThreadPool, Signal
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
@@ -15,8 +15,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from src.core.utils.missing_files import MissingRegistry
|
||||
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.
|
||||
@@ -77,9 +75,14 @@ class DeleteUnlinkedEntriesModal(QWidget):
|
||||
|
||||
self.model.clear()
|
||||
for i in self.tracker.missing_files:
|
||||
self.model.appendRow(QStandardItem(str(i.path)))
|
||||
item = QStandardItem(str(i.path))
|
||||
item.setEditable(False)
|
||||
self.model.appendRow(item)
|
||||
|
||||
def delete_entries(self):
|
||||
def displayed_text(x):
|
||||
return f"Deleting {x}/{self.tracker.missing_files_count} Unlinked Entries"
|
||||
|
||||
pw = ProgressWidget(
|
||||
window_title="Deleting Entries",
|
||||
label_text="",
|
||||
@@ -87,23 +90,5 @@ class DeleteUnlinkedEntriesModal(QWidget):
|
||||
minimum=0,
|
||||
maximum=self.tracker.missing_files_count,
|
||||
)
|
||||
pw.show()
|
||||
|
||||
iterator = FunctionIterator(self.tracker.execute_deletion)
|
||||
files_count = self.tracker.missing_files_count
|
||||
iterator.value.connect(
|
||||
lambda idx: (
|
||||
pw.update_progress(idx),
|
||||
pw.update_label(f"Deleting {idx}/{files_count} Unlinked Entries"),
|
||||
)
|
||||
)
|
||||
|
||||
r = CustomRunnable(iterator.run)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
r.done.connect(
|
||||
lambda: (
|
||||
pw.hide(),
|
||||
pw.deleteLater(),
|
||||
self.done.emit(),
|
||||
)
|
||||
)
|
||||
pw.from_iterable_function(self.tracker.execute_deletion, displayed_text, self.done.emit)
|
||||
|
||||
229
tagstudio/src/qt/modals/drop_import.py
Normal file
229
tagstudio/src/qt/modals/drop_import.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
import enum
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Qt, QUrl
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListView,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class DuplicateChoice(enum.StrEnum):
|
||||
SKIP = "Skipped"
|
||||
OVERWRITE = "Overwritten"
|
||||
RENAME = "Renamed"
|
||||
CANCEL = "Cancelled"
|
||||
|
||||
|
||||
class DropImportModal(QWidget):
|
||||
DUPE_NAME_LIMT: int = 5
|
||||
|
||||
def __init__(self, driver: "QtDriver"):
|
||||
super().__init__()
|
||||
|
||||
self.driver: QtDriver = driver
|
||||
|
||||
# Widget ======================
|
||||
self.setWindowTitle("Conflicting File(s)")
|
||||
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
self.setMinimumSize(500, 400)
|
||||
self.root_layout = QVBoxLayout(self)
|
||||
self.root_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
self.desc_widget = QLabel()
|
||||
self.desc_widget.setObjectName("descriptionLabel")
|
||||
self.desc_widget.setWordWrap(True)
|
||||
self.desc_widget.setText("The following files have filenames already exist in the library")
|
||||
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# Duplicate File List ========
|
||||
self.list_view = QListView()
|
||||
self.model = QStandardItemModel()
|
||||
self.list_view.setModel(self.model)
|
||||
|
||||
# Buttons ====================
|
||||
self.button_container = QWidget()
|
||||
self.button_layout = QHBoxLayout(self.button_container)
|
||||
self.button_layout.setContentsMargins(6, 6, 6, 6)
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.skip_button = QPushButton()
|
||||
self.skip_button.setText("&Skip")
|
||||
self.skip_button.setDefault(True)
|
||||
self.skip_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.SKIP))
|
||||
self.button_layout.addWidget(self.skip_button)
|
||||
|
||||
self.overwrite_button = QPushButton()
|
||||
self.overwrite_button.setText("&Overwrite")
|
||||
self.overwrite_button.clicked.connect(
|
||||
lambda: self.begin_transfer(DuplicateChoice.OVERWRITE)
|
||||
)
|
||||
self.button_layout.addWidget(self.overwrite_button)
|
||||
|
||||
self.rename_button = QPushButton()
|
||||
self.rename_button.setText("&Rename")
|
||||
self.rename_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.RENAME))
|
||||
self.button_layout.addWidget(self.rename_button)
|
||||
|
||||
self.cancel_button = QPushButton()
|
||||
self.cancel_button.setText("&Cancel")
|
||||
self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL))
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
# Layout =====================
|
||||
self.root_layout.addWidget(self.desc_widget)
|
||||
self.root_layout.addWidget(self.list_view)
|
||||
self.root_layout.addWidget(self.button_container)
|
||||
|
||||
def import_urls(self, urls: list[QUrl]):
|
||||
"""Add a colleciton of urls to the library."""
|
||||
self.files: list[Path] = []
|
||||
self.dirs_in_root: list[Path] = []
|
||||
self.duplicate_files: list[Path] = []
|
||||
|
||||
self.collect_files_to_import(urls)
|
||||
|
||||
if len(self.duplicate_files) > 0:
|
||||
self.ask_duplicates_choice()
|
||||
else:
|
||||
self.begin_transfer()
|
||||
|
||||
def collect_files_to_import(self, urls: list[QUrl]):
|
||||
"""Collect one or more files from drop event urls."""
|
||||
for url in urls:
|
||||
if not url.isLocalFile():
|
||||
continue
|
||||
|
||||
file = Path(url.toLocalFile())
|
||||
|
||||
if file.is_dir():
|
||||
for f in file.glob("**/*"):
|
||||
if f.is_dir():
|
||||
continue
|
||||
|
||||
self.files.append(f)
|
||||
if (self.driver.lib.library_dir / self._get_relative_path(file)).exists():
|
||||
self.duplicate_files.append(f)
|
||||
|
||||
self.dirs_in_root.append(file.parent)
|
||||
else:
|
||||
self.files.append(file)
|
||||
|
||||
if file.parent not in self.dirs_in_root:
|
||||
self.dirs_in_root.append(
|
||||
file.parent
|
||||
) # to create relative path of files not in folder
|
||||
|
||||
if (Path(self.driver.lib.library_dir) / file.name).exists():
|
||||
self.duplicate_files.append(file)
|
||||
|
||||
def ask_duplicates_choice(self):
|
||||
"""Display the message widgeth with a list of the duplicated files."""
|
||||
self.desc_widget.setText(
|
||||
f"The following {len(self.duplicate_files)} file(s) have filenames already exist in the library." # noqa: E501
|
||||
)
|
||||
|
||||
self.model.clear()
|
||||
for dupe in self.duplicate_files:
|
||||
item = QStandardItem(str(self._get_relative_path(dupe)))
|
||||
item.setEditable(False)
|
||||
self.model.appendRow(item)
|
||||
|
||||
self.driver.main_window.raise_()
|
||||
self.show()
|
||||
|
||||
def begin_transfer(self, choice: DuplicateChoice | None = None):
|
||||
"""Display a progress bar and begin copying files into library."""
|
||||
self.hide()
|
||||
self.choice: DuplicateChoice | None = choice
|
||||
logger.info("duplicated choice selected", choice=self.choice)
|
||||
if self.choice == DuplicateChoice.CANCEL:
|
||||
return
|
||||
|
||||
def displayed_text(x):
|
||||
text = (
|
||||
f"Importing New Files...\n{x[0] + 1} File{'s' if x[0] + 1 != 1 else ''} Imported."
|
||||
)
|
||||
if self.choice:
|
||||
text += f" {x[1]} {self.choice.value}"
|
||||
|
||||
return text
|
||||
|
||||
pw = ProgressWidget(
|
||||
window_title="Import Files",
|
||||
label_text="Importing New Files...",
|
||||
cancel_button_text=None,
|
||||
minimum=0,
|
||||
maximum=len(self.files),
|
||||
)
|
||||
|
||||
pw.from_iterable_function(
|
||||
self.copy_files,
|
||||
displayed_text,
|
||||
self.driver.add_new_files_callback,
|
||||
self.deleteLater,
|
||||
)
|
||||
|
||||
def copy_files(self):
|
||||
"""Copy files from original location to the library directory."""
|
||||
file_count = 0
|
||||
duplicated_files_progress = 0
|
||||
for file in self.files:
|
||||
if file.is_dir():
|
||||
continue
|
||||
|
||||
dest_file = self._get_relative_path(file)
|
||||
|
||||
if file in self.duplicate_files:
|
||||
duplicated_files_progress += 1
|
||||
if self.choice == DuplicateChoice.SKIP:
|
||||
file_count += 1
|
||||
continue
|
||||
elif self.choice == DuplicateChoice.RENAME:
|
||||
new_name = self._get_renamed_duplicate_filename(dest_file)
|
||||
dest_file = dest_file.with_name(new_name)
|
||||
|
||||
(self.driver.lib.library_dir / dest_file).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(file, self.driver.lib.library_dir / dest_file)
|
||||
|
||||
file_count += 1
|
||||
yield [file_count, duplicated_files_progress]
|
||||
|
||||
def _get_relative_path(self, path: Path) -> Path:
|
||||
for dir in self.dirs_in_root:
|
||||
if path.is_relative_to(dir):
|
||||
return path.relative_to(dir)
|
||||
return Path(path.name)
|
||||
|
||||
def _get_renamed_duplicate_filename(self, filepath: Path) -> str:
|
||||
index = 2
|
||||
o_filename = filepath.name
|
||||
|
||||
try:
|
||||
dot_idx = o_filename.index(".")
|
||||
except ValueError:
|
||||
dot_idx = len(o_filename)
|
||||
|
||||
while (self.driver.lib.library_dir / filepath).exists():
|
||||
filepath = filepath.with_name(
|
||||
o_filename[:dot_idx] + f" ({index})" + o_filename[dot_idx:]
|
||||
)
|
||||
index += 1
|
||||
return filepath.name
|
||||
@@ -5,12 +5,10 @@
|
||||
|
||||
import typing
|
||||
|
||||
from PySide6.QtCore import Qt, QThreadPool
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
|
||||
from src.core.library import Library
|
||||
from src.core.utils.missing_files import MissingRegistry
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal
|
||||
from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries
|
||||
from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries
|
||||
@@ -85,7 +83,7 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
self.delete_modal = DeleteUnlinkedEntriesModal(self.driver, self.tracker)
|
||||
self.delete_modal.done.connect(
|
||||
lambda: (
|
||||
self.set_missing_count(self.tracker.missing_files_count),
|
||||
self.set_missing_count(),
|
||||
# refresh the grid
|
||||
self.driver.filter_items(),
|
||||
)
|
||||
@@ -125,23 +123,19 @@ class FixUnlinkedEntriesModal(QWidget):
|
||||
maximum=self.lib.entries_count,
|
||||
)
|
||||
|
||||
pw.show()
|
||||
|
||||
iterator = FunctionIterator(self.tracker.refresh_missing_files)
|
||||
iterator.value.connect(lambda v: pw.update_progress(v + 1))
|
||||
r = CustomRunnable(iterator.run)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
r.done.connect(
|
||||
lambda: (
|
||||
pw.hide(),
|
||||
pw.deleteLater(),
|
||||
self.set_missing_count(self.tracker.missing_files_count),
|
||||
self.delete_modal.refresh_list(),
|
||||
)
|
||||
pw.from_iterable_function(
|
||||
self.tracker.refresh_missing_files,
|
||||
None,
|
||||
self.set_missing_count,
|
||||
self.delete_modal.refresh_list,
|
||||
)
|
||||
|
||||
def set_missing_count(self, count: int):
|
||||
self.missing_count = count
|
||||
def set_missing_count(self, count: int | None = None):
|
||||
if count is not None:
|
||||
self.missing_count = count
|
||||
else:
|
||||
self.missing_count = self.tracker.missing_files_count
|
||||
|
||||
if self.missing_count < 0:
|
||||
self.search_button.setDisabled(True)
|
||||
self.delete_button.setDisabled(True)
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
|
||||
import typing
|
||||
|
||||
from PySide6.QtCore import QObject, QThreadPool, Signal
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from src.core.library import Library
|
||||
from src.core.utils.dupe_files import DupeRegistry
|
||||
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.
|
||||
@@ -26,20 +24,12 @@ class MergeDuplicateEntries(QObject):
|
||||
self.tracker = DupeRegistry(library=self.lib)
|
||||
|
||||
def merge_entries(self):
|
||||
iterator = FunctionIterator(self.tracker.merge_dupe_entries)
|
||||
|
||||
pw = ProgressWidget(
|
||||
window_title="Merging Duplicate Entries",
|
||||
label_text="",
|
||||
label_text="Merging Duplicate Entries...",
|
||||
cancel_button_text=None,
|
||||
minimum=0,
|
||||
maximum=self.tracker.groups_count,
|
||||
)
|
||||
pw.show()
|
||||
|
||||
iterator.value.connect(lambda x: pw.update_progress(x))
|
||||
iterator.value.connect(lambda: (pw.update_label("Merging Duplicate Entries...")))
|
||||
|
||||
r = CustomRunnable(iterator.run)
|
||||
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
|
||||
QThreadPool.globalInstance().start(r)
|
||||
pw.from_iterable_function(self.tracker.merge_dupe_entries, None, self.done.emit)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import typing
|
||||
from time import sleep
|
||||
|
||||
from PySide6.QtCore import Qt, QThreadPool, Signal
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
@@ -17,8 +17,6 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from src.core.utils.dupe_files import DupeRegistry
|
||||
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.
|
||||
@@ -83,28 +81,22 @@ class MirrorEntriesModal(QWidget):
|
||||
self.model.appendRow(QStandardItem(str(i)))
|
||||
|
||||
def mirror_entries(self):
|
||||
iterator = FunctionIterator(self.mirror_entries_runnable)
|
||||
def displayed_text(x):
|
||||
return f"Mirroring {x + 1}/{self.tracker.groups_count} Entries..."
|
||||
|
||||
pw = ProgressWidget(
|
||||
window_title="Mirroring Entries",
|
||||
label_text=f"Mirroring 1/{self.tracker.groups_count} Entries...",
|
||||
label_text="",
|
||||
cancel_button_text=None,
|
||||
minimum=0,
|
||||
maximum=self.tracker.groups_count,
|
||||
)
|
||||
pw.show()
|
||||
iterator.value.connect(lambda x: pw.update_progress(x + 1))
|
||||
iterator.value.connect(
|
||||
lambda x: pw.update_label(f"Mirroring {x + 1}/{self.tracker.groups_count} Entries...")
|
||||
)
|
||||
r = CustomRunnable(iterator.run)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
r.done.connect(
|
||||
lambda: (
|
||||
pw.hide(),
|
||||
pw.deleteLater(),
|
||||
self.driver.preview_panel.update_widgets(),
|
||||
self.done.emit(),
|
||||
)
|
||||
|
||||
pw.from_iterable_function(
|
||||
self.mirror_entries_runnable,
|
||||
displayed_text,
|
||||
self.driver.preview_panel.update_widgets,
|
||||
self.done.emit,
|
||||
)
|
||||
|
||||
def mirror_entries_runnable(self):
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
from PySide6.QtCore import QObject, QThreadPool, Signal
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from src.core.utils.missing_files import MissingRegistry
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.widgets.progress import ProgressWidget
|
||||
|
||||
|
||||
@@ -18,7 +16,10 @@ class RelinkUnlinkedEntries(QObject):
|
||||
self.tracker = tracker
|
||||
|
||||
def repair_entries(self):
|
||||
iterator = FunctionIterator(self.tracker.fix_missing_files)
|
||||
def displayed_text(x):
|
||||
text = f"Attempting to Relink {x}/{self.tracker.missing_files_count} Entries. \n"
|
||||
text += f"{self.tracker.files_fixed_count} Successfully Relinked."
|
||||
return text
|
||||
|
||||
pw = ProgressWidget(
|
||||
window_title="Relinking Entries",
|
||||
@@ -28,24 +29,4 @@ class RelinkUnlinkedEntries(QObject):
|
||||
maximum=self.tracker.missing_files_count,
|
||||
)
|
||||
|
||||
pw.show()
|
||||
|
||||
iterator.value.connect(
|
||||
lambda idx: (
|
||||
pw.update_progress(idx),
|
||||
pw.update_label(
|
||||
f"Attempting to Relink {idx}/{self.tracker.missing_files_count} Entries. "
|
||||
f"{self.tracker.files_fixed_count} Successfully Relinked."
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
r = CustomRunnable(iterator.run)
|
||||
r.done.connect(
|
||||
lambda: (
|
||||
pw.hide(),
|
||||
pw.deleteLater(),
|
||||
self.done.emit(),
|
||||
)
|
||||
)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
pw.from_iterable_function(self.tracker.fix_missing_files, displayed_text, self.done.emit)
|
||||
|
||||
@@ -37,6 +37,9 @@ from PySide6.QtCore import (
|
||||
from PySide6.QtGui import (
|
||||
QAction,
|
||||
QColor,
|
||||
QDragEnterEvent,
|
||||
QDragMoveEvent,
|
||||
QDropEvent,
|
||||
QFontDatabase,
|
||||
QGuiApplication,
|
||||
QIcon,
|
||||
@@ -82,6 +85,7 @@ from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
from src.qt.main_window import Ui_MainWindow
|
||||
from src.qt.modals.build_tag import BuildTagPanel
|
||||
from src.qt.modals.drop_import import DropImportModal
|
||||
from src.qt.modals.file_extension import FileExtensionModal
|
||||
from src.qt.modals.fix_dupes import FixDupeFilesModal
|
||||
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
|
||||
@@ -234,19 +238,10 @@ class QtDriver(DriverMixin, QObject):
|
||||
# self.main_window = loader.load(home_path)
|
||||
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(
|
||||
# f'QScrollBar::{{background:red;}}'
|
||||
# )
|
||||
|
||||
# # self.main_window.windowFlags() &
|
||||
# # self.main_window.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
|
||||
# self.main_window.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True)
|
||||
# self.main_window.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False)
|
||||
# self.main_window.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
# self.windowFX = WindowEffect()
|
||||
# self.windowFX.setAcrylicEffect(self.main_window.winId())
|
||||
self.main_window.mousePressEvent = self.mouse_navigation # type: ignore[method-assign]
|
||||
self.main_window.dragEnterEvent = self.drag_enter_event # type: ignore[method-assign]
|
||||
self.main_window.dragMoveEvent = self.drag_move_event # type: ignore[method-assign]
|
||||
self.main_window.dropEvent = self.drop_event # type: ignore[method-assign]
|
||||
|
||||
splash_pixmap = QPixmap(":/images/splash.png")
|
||||
splash_pixmap.setDevicePixelRatio(self.main_window.devicePixelRatio())
|
||||
@@ -716,26 +711,6 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
Threaded method.
|
||||
"""
|
||||
# pb = QProgressDialog(
|
||||
# f"Running Configured Macros on 1/{len(new_ids)} New Entries", None, 0, len(new_ids)
|
||||
# )
|
||||
# pb.setFixedSize(432, 112)
|
||||
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
|
||||
# pb.setWindowTitle('Running Macros')
|
||||
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
|
||||
# pb.show()
|
||||
|
||||
# r = CustomRunnable(lambda: self.new_file_macros_runnable(pb, new_ids))
|
||||
# r.done.connect(lambda: (pb.hide(), pb.deleteLater(), self.filter_items('')))
|
||||
# r.run()
|
||||
# # QThreadPool.globalInstance().start(r)
|
||||
|
||||
# # self.main_window.statusbar.showMessage(
|
||||
# # f"Running configured Macros on {len(new_ids)} new Entries...", 3
|
||||
# # )
|
||||
|
||||
# # pb.hide()
|
||||
|
||||
files_count = tracker.files_count
|
||||
|
||||
iterator = FunctionIterator(tracker.save_new_files)
|
||||
@@ -747,6 +722,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
maximum=files_count,
|
||||
)
|
||||
pw.show()
|
||||
|
||||
iterator.value.connect(
|
||||
lambda x: (
|
||||
pw.update_progress(x + 1),
|
||||
@@ -905,6 +881,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
item_thumb = ItemThumb(
|
||||
None, self.lib, self, (self.thumb_size, self.thumb_size), grid_idx
|
||||
)
|
||||
|
||||
layout.addWidget(item_thumb)
|
||||
self.item_thumbs.append(item_thumb)
|
||||
|
||||
@@ -1234,6 +1211,7 @@ class QtDriver(DriverMixin, QObject):
|
||||
self.update_libs_list(path)
|
||||
title_text = f"{self.base_title} - Library '{self.lib.library_dir}'"
|
||||
self.main_window.setWindowTitle(title_text)
|
||||
self.main_window.setAcceptDrops(True)
|
||||
|
||||
self.selected.clear()
|
||||
self.preview_panel.update_widgets()
|
||||
@@ -1243,3 +1221,27 @@ class QtDriver(DriverMixin, QObject):
|
||||
|
||||
self.main_window.toggle_landing_page(enabled=False)
|
||||
return open_status
|
||||
|
||||
def drop_event(self, event: QDropEvent):
|
||||
if event.source() is self:
|
||||
return
|
||||
|
||||
if not event.mimeData().hasUrls():
|
||||
return
|
||||
|
||||
urls = event.mimeData().urls()
|
||||
logger.info("New items dragged in", urls=urls)
|
||||
drop_import = DropImportModal(self)
|
||||
drop_import.import_urls(urls)
|
||||
|
||||
def drag_enter_event(self, event: QDragEnterEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def drag_move_event(self, event: QDragMoveEvent):
|
||||
if event.mimeData().hasUrls():
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
@@ -10,8 +10,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageQt
|
||||
from PySide6.QtCore import QEvent, QSize, Qt
|
||||
from PySide6.QtGui import QAction, QEnterEvent, QPixmap
|
||||
from PySide6.QtCore import QEvent, QMimeData, QSize, Qt, QUrl
|
||||
from PySide6.QtGui import QAction, QDrag, QEnterEvent, QPixmap
|
||||
from PySide6.QtWidgets import (
|
||||
QBoxLayout,
|
||||
QCheckBox,
|
||||
@@ -127,6 +127,7 @@ class ItemThumb(FlowWidget):
|
||||
self.thumb_size: tuple[int, int] = thumb_size
|
||||
self.setMinimumSize(*thumb_size)
|
||||
self.setMaximumSize(*thumb_size)
|
||||
self.setMouseTracking(True)
|
||||
check_size = 24
|
||||
|
||||
# +----------+
|
||||
@@ -480,3 +481,29 @@ class ItemThumb(FlowWidget):
|
||||
|
||||
if self.driver.preview_panel.is_open:
|
||||
self.driver.preview_panel.update_widgets()
|
||||
|
||||
def mouseMoveEvent(self, event): # noqa: N802
|
||||
if event.buttons() is not Qt.MouseButton.LeftButton:
|
||||
return
|
||||
|
||||
drag = QDrag(self.driver)
|
||||
paths = []
|
||||
mimedata = QMimeData()
|
||||
|
||||
selected_idxs = self.driver.selected
|
||||
if self.grid_idx not in selected_idxs:
|
||||
selected_idxs = [self.grid_idx]
|
||||
|
||||
for grid_idx in selected_idxs:
|
||||
id = self.driver.item_thumbs[grid_idx].item_id
|
||||
entry = self.lib.get_entry(id)
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
url = QUrl.fromLocalFile(Path(self.lib.library_dir) / entry.path)
|
||||
paths.append(url)
|
||||
|
||||
mimedata.setUrls(paths)
|
||||
drag.setMimeData(mimedata)
|
||||
drag.exec(Qt.DropAction.CopyAction)
|
||||
logger.info("dragged files to external program", thumbnail_indexs=selected_idxs)
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
from typing import Callable, Optional
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import Qt, QThreadPool
|
||||
from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget
|
||||
from src.qt.helpers.custom_runnable import CustomRunnable
|
||||
from src.qt.helpers.function_iterator import FunctionIterator
|
||||
|
||||
|
||||
class ProgressWidget(QWidget):
|
||||
@@ -39,3 +40,26 @@ class ProgressWidget(QWidget):
|
||||
|
||||
def update_progress(self, value: int):
|
||||
self.pb.setValue(value)
|
||||
|
||||
def _update_progress_unknown_iterable(self, value):
|
||||
if hasattr(value, "__getitem__"):
|
||||
self.update_progress(value[0] + 1)
|
||||
else:
|
||||
self.update_progress(value + 1)
|
||||
|
||||
def from_iterable_function(
|
||||
self, function: Callable, update_label_callback: Callable | None, *done_callbacks
|
||||
):
|
||||
"""Display the progress widget from a threaded iterable function."""
|
||||
iterator = FunctionIterator(function)
|
||||
iterator.value.connect(lambda x: self._update_progress_unknown_iterable(x))
|
||||
if update_label_callback:
|
||||
iterator.value.connect(lambda x: self.update_label(update_label_callback(x)))
|
||||
|
||||
self.show()
|
||||
|
||||
r = CustomRunnable(lambda: iterator.run())
|
||||
r.done.connect(
|
||||
lambda: (self.hide(), self.deleteLater(), [callback() for callback in done_callbacks])
|
||||
)
|
||||
QThreadPool.globalInstance().start(r)
|
||||
|
||||
Reference in New Issue
Block a user