Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Reimplement drag drop files on sql migration #528

Merged
merged 3 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 8 additions & 23 deletions tagstudio/src/qt/modals/delete_unlinked.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,8 +15,6 @@
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.
Expand Down Expand Up @@ -77,33 +75,20 @@ def refresh_list(self):

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="",
cancel_button_text=None,
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 changes: 229 additions & 0 deletions tagstudio/src/qt/modals/drop_import.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an action is required, I'd expect the window will gain focus. So I'd add self.driver.main_window.raise_() here. Otherwise the result might look like this, so I might not be even aware the drag&drop action havent finished:

Screenshot 2024-11-10 at 18 54 36

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, but window already raised on my system so unsure if that is fixed for you


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
32 changes: 13 additions & 19 deletions tagstudio/src/qt/modals/fix_unlinked.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -85,7 +83,7 @@ def __init__(self, library: "Library", driver: "QtDriver"):
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(),
)
Expand Down Expand Up @@ -125,23 +123,19 @@ def refresh_missing_files(self):
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)
Expand Down
16 changes: 3 additions & 13 deletions tagstudio/src/qt/modals/merge_dupe_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,20 +24,12 @@ def __init__(self, library: "Library", driver: "QtDriver"):
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)
Loading