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

Drag and drop files in and out of TagStudio #153

Merged
merged 38 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dccf5f2
Ability to drop local files in to TagStudio to add to library
Creepler13 May 9, 2024
44c6a96
Added renaming option to drop import
Creepler13 May 9, 2024
95fe393
Improved readability and switched to pathLib
Creepler13 May 10, 2024
c3793d5
format
Creepler13 May 10, 2024
f0cf853
Apply suggestions from code review
Creepler13 May 10, 2024
f7eaec7
Merge branch 'TagStudioDev:main' into DropFiles
Creepler13 May 10, 2024
e835a00
Revert Change
Creepler13 May 10, 2024
741b2d6
Update tagstudio/src/qt/modals/drop_import.py
Creepler13 May 10, 2024
5149855
Added support for folders
Creepler13 May 12, 2024
12c4c43
formatting
Creepler13 May 12, 2024
aeb119e
Progress bars added
Creepler13 May 12, 2024
dbbe1b0
Added Ability to Drag out of window
Creepler13 May 22, 2024
9a45ae1
f
Creepler13 May 22, 2024
1a70369
format
Creepler13 May 22, 2024
0ac25d6
Ability to drop local files in to TagStudio to add to library
Creepler13 May 9, 2024
d7a9305
Added renaming option to drop import
Creepler13 May 9, 2024
f9d1b55
Improved readability and switched to pathLib
Creepler13 May 10, 2024
374ddbc
format
Creepler13 May 10, 2024
5b7f209
Apply suggestions from code review
Creepler13 May 10, 2024
76879da
Revert Change
Creepler13 May 10, 2024
cf1402f
Update tagstudio/src/qt/modals/drop_import.py
Creepler13 May 10, 2024
db8d94e
Added support for folders
Creepler13 May 12, 2024
75acd23
formatting
Creepler13 May 12, 2024
8f8b93b
Progress bars added
Creepler13 May 12, 2024
c7f420b
Added Ability to Drag out of window
Creepler13 May 22, 2024
40873ff
f
Creepler13 May 22, 2024
234a9cb
format
Creepler13 May 22, 2024
46b706b
Rebase
Creepler13 May 22, 2024
06774b6
format
Creepler13 May 22, 2024
b453c11
formatting and refactor
Creepler13 May 22, 2024
2020985
format again
Creepler13 May 22, 2024
acb27c7
formatting for mypy
Creepler13 May 22, 2024
fb1c3a2
convert lambda to func for clarity
Creepler13 May 24, 2024
dfbf31f
mypy fixes
Creepler13 May 24, 2024
45056f1
fixed dragout only worked on selected
Creepler13 May 24, 2024
301a7b0
Refactor typo, Add license
CyanVoxel Jun 13, 2024
763d189
Reformat QMessageBox
CyanVoxel Jun 13, 2024
9549d92
Disable drops when no library is open
CyanVoxel Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
251 changes: 251 additions & 0 deletions tagstudio/src/qt/modals/drop_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
from pathlib import Path
import shutil
import typing

from PySide6.QtCore import QThreadPool, Qt, QMimeData, QUrl
from PySide6.QtGui import (
QDropEvent,
QDragEnterEvent,
QDragMoveEvent,
QMouseEvent,
QDrag,
QDragLeaveEvent,
)
from PySide6.QtWidgets import QMessageBox
from src.qt.widgets.progress import ProgressWidget
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator

if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver

import logging


class DropImport:
def __init__(self, driver: "QtDriver"):
self.driver = driver

def mouseMoveEvent(self, event: QMouseEvent):
if event.buttons() is not Qt.MouseButton.LeftButton:
return
if len(self.driver.selected) == 0:
return

drag = QDrag(self.driver)
paths = []
mimedata = QMimeData()
for selected in self.driver.selected:
entry = self.driver.lib.get_entry(selected[1])
url = QUrl.fromLocalFile(
Path(self.driver.lib.library_dir) / entry.path / entry.filename
)
paths.append(url)

mimedata.setUrls(paths)
drag.setMimeData(mimedata)
drag.exec(Qt.DropAction.CopyAction)

def dropEvent(self, event: QDropEvent):
if (
event.source() is self.driver
): # change that if you want to drop something originating from tagstudio, for moving or so
return

if not event.mimeData().hasUrls():
return

self.urls = event.mimeData().urls()
self.import_files()

def dragLeaveEvent(self, event: QDragLeaveEvent):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()

def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()

def dragMoveEvent(self, event: QDragMoveEvent):
if event.mimeData().hasUrls():
event.accept()
else:
logging.info(self.driver.selected)
event.ignore()

def import_files(self):
self.files: list[Path] = []
self.dirs_in_root: list[Path] = []
self.duplicate_files: list[Path] = []

create_progress_bar(
self.collect_files_to_import,
"Searching Files",
"Searching New Files...\nPreparing...",
lambda x: f'Searching New Files...\n{x[0]+1} File{'s' if x[0]+1 != 1 else ''} Found. {(f'{x[1]} Already exist in the library folders') if x[1]>0 else ''}',

Check failure on line 89 in tagstudio/src/qt/modals/drop_import.py

View workflow job for this annotation

GitHub Actions / Run MyPy

[mypy] reported by reviewdog 🐶 f-string: expecting '}' [syntax] Raw Output: /home/runner/work/TagStudio/TagStudio/tagstudio/src/qt/modals/drop_import.py:89:65: error: f-string: expecting '}' [syntax]
Creepler13 marked this conversation as resolved.
Show resolved Hide resolved
self.ask_user,
)

def collect_files_to_import(self):
for url in self.urls:
if not url.isLocalFile():
continue

file = Path(url.toLocalFile())

if file.is_dir():
for f in self.get_files_in_folder(file):
if f.is_dir():
continue
self.files.append(f)
if (
self.driver.lib.library_dir / self.get_relativ_path(file)
).exists():
self.duplicate_files.append(f)
yield [len(self.files), len(self.duplicate_files)]

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)

yield [len(self.files), len(self.duplicate_files)]

def copy_files(self):
fileCount = 0
duplicated_files_progress = 0
for file in self.files:
if file.is_dir():
continue

dest_file = self.get_relativ_path(file)

if file in self.duplicate_files:
duplicated_files_progress += 1
if self.choice == 0: # skip duplicates
continue

if self.choice == 2: # rename
new_name = self.get_renamed_duplicate_filename_in_lib(dest_file)
dest_file = dest_file.with_name(new_name)
self.driver.lib.files_not_in_library.append(dest_file)
else: # override is simply copying but not adding a new entry
self.driver.lib.files_not_in_library.append(dest_file)

(self.driver.lib.library_dir / dest_file).parent.mkdir(
parents=True, exist_ok=True
)
shutil.copyfile(file, self.driver.lib.library_dir / dest_file)

fileCount += 1
yield [fileCount, duplicated_files_progress]

def ask_user(self):
self.choice = -1

if len(self.duplicate_files) > 0:
self.choice = self.duplicates_choice()

if self.choice == 3: # cancel
return

dupes_choice_text = (
"Skipped"
if self.choice == 0
else ("Overridden" if self.choice == 1 else "Renamed")
)
create_progress_bar(
self.copy_files,
"Import Files",
"Importing New Files...\nPreparing...",
lambda x: f'Importing New Files...\n{x[0]+1} File{'s' if x[0]+1 != 1 else ''} Imported. {(f'{x[1]} {dupes_choice_text}') if x[1]>0 else ''}',
self.driver.add_new_files_runnable,
len(self.files),
)

def duplicates_choice(self) -> int:
msgBox = QMessageBox()
dupes_to_show = self.duplicate_files
if len(self.duplicate_files) > 20:
dupes_to_show = dupes_to_show[0:20]

msgBox.setText(
f"The files {', '.join(map(lambda path: str(path),self.get_relativ_paths(dupes_to_show)))} {(f'and {len(self.duplicate_files)-20} more') if len(self.duplicate_files)>20 else ''} have filenames that already exist in the library folder."
)
msgBox.addButton("Skip", QMessageBox.ButtonRole.YesRole)
msgBox.addButton("Override", QMessageBox.ButtonRole.DestructiveRole)
msgBox.addButton("Rename", QMessageBox.ButtonRole.DestructiveRole)
msgBox.addButton("Cancel", QMessageBox.ButtonRole.NoRole)
return msgBox.exec()

def get_files_exists_in_library(self, path: Path) -> list[Path]:
exists: list[Path] = []
if not path.is_dir():
return exists

files = self.get_files_in_folder(path)
for file in files:
if file.is_dir():
exists += self.get_files_exists_in_library(file)
elif (self.driver.lib.library_dir / self.get_relativ_path(file)).exists():
exists.append(file)
return exists

def get_relativ_paths(self, paths: list[Path]) -> list[Path]:
relativ_paths = []
for file in paths:
relativ_paths.append(self.get_relativ_path(file))
return relativ_paths

def get_relativ_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_files_in_folder(self, path: Path) -> list[Path]:
files = []
for file in path.glob("**/*"):
files.append(file)
return files

def get_renamed_duplicate_filename_in_lib(self, filePath: Path) -> str:
index = 2
o_filename = filePath.name
dot_idx = o_filename.index(".")
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


def create_progress_bar(
function, title: str, text: str, update_label_callback, done_callback, max=0
):
iterator = FunctionIterator(function)
pw = ProgressWidget(
window_title=title,
label_text=text,
cancel_button_text=None,
minimum=0,
maximum=max,
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0] + 1))
iterator.value.connect(lambda x: pw.update_label(update_label_callback(x)))
r = CustomRunnable(lambda: iterator.run())
r.done.connect(lambda: (pw.hide(), done_callback()))
QThreadPool.globalInstance().start(r)
11 changes: 11 additions & 0 deletions tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
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
from src.qt.modals.drop_import import DropImport

# this import has side-effect of import PySide resources
import src.qt.resources_rc # pylint: disable=unused-import
Expand Down Expand Up @@ -271,6 +272,12 @@ def start(self) -> None:
# f'QScrollBar::{{background:red;}}'
# )

self.drop_import = DropImport(self)
self.main_window.setAcceptDrops(True)
self.main_window.dragEnterEvent = self.drop_import.dragEnterEvent
self.main_window.dropEvent = self.drop_import.dropEvent
self.main_window.dragMoveEvent = self.drop_import.dragMoveEvent

# # self.main_window.windowFlags() &
# # self.main_window.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
# self.main_window.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True)
Expand Down Expand Up @@ -1059,6 +1066,10 @@ def _init_thumb_grid(self):
item_thumb = ItemThumb(
None, self.lib, self.preview_panel, (self.thumb_size, self.thumb_size)
)

item_thumb.setMouseTracking(True)
item_thumb.mouseMoveEvent = self.drop_import.mouseMoveEvent

layout.addWidget(item_thumb)
self.item_thumbs.append(item_thumb)

Expand Down