From 653a35496d5196d6ecfcefe1ca8569f1b770b95f Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Wed, 19 Aug 2020 22:02:35 +0200 Subject: [PATCH] Add optional delete from disk in collection and playlist Fixes #284 --- src/CMakeLists.txt | 2 + src/collection/collectionview.cpp | 132 +++++++++++++++++------ src/collection/collectionview.h | 31 +++--- src/core/deletefiles.cpp | 4 +- src/core/deletefiles.h | 3 +- src/core/filesystemmusicstorage.cpp | 11 ++ src/core/mainwindow.cpp | 72 +++++++++++-- src/core/mainwindow.h | 10 +- src/core/musicstorage.h | 3 + src/core/utilities.cpp | 24 +++++ src/core/utilities.h | 1 + src/device/deviceview.cpp | 2 +- src/dialogs/deleteconfirmationdialog.cpp | 114 ++++++++++++++++++++ src/dialogs/deleteconfirmationdialog.h | 43 ++++++++ src/settings/collectionsettingspage.cpp | 4 + src/settings/collectionsettingspage.ui | 7 ++ src/settings/playlistsettingspage.cpp | 3 + src/settings/playlistsettingspage.ui | 7 ++ src/widgets/fileview.cpp | 25 +---- 19 files changed, 411 insertions(+), 87 deletions(-) create mode 100644 src/dialogs/deleteconfirmationdialog.cpp create mode 100644 src/dialogs/deleteconfirmationdialog.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f8024f97b..0c36d12cc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -169,6 +169,7 @@ set(SOURCES dialogs/trackselectiondialog.cpp dialogs/addstreamdialog.cpp dialogs/userpassdialog.cpp + dialogs/deleteconfirmationdialog.cpp widgets/autoexpandingtreeview.cpp widgets/busyindicator.cpp @@ -367,6 +368,7 @@ set(HEADERS dialogs/trackselectiondialog.h dialogs/addstreamdialog.h dialogs/userpassdialog.h + dialogs/deleteconfirmationdialog.h widgets/autoexpandingtreeview.h widgets/busyindicator.h diff --git a/src/collection/collectionview.cpp b/src/collection/collectionview.cpp index 055c9a240..2350750c9 100644 --- a/src/collection/collectionview.cpp +++ b/src/collection/collectionview.cpp @@ -48,6 +48,7 @@ #include "core/iconloader.h" #include "core/mimedata.h" #include "core/utilities.h" +#include "core/deletefiles.h" #include "collection.h" #include "collectionbackend.h" #include "collectiondirectorymodel.h" @@ -61,7 +62,9 @@ # include "device/devicestatefiltermodel.h" #endif #include "dialogs/edittagdialog.h" +#include "dialogs/deleteconfirmationdialog.h" #include "organize/organizedialog.h" +#include "organize/organizeerrordialog.h" #include "settings/collectionsettingspage.h" CollectionView::CollectionView(QWidget *parent) @@ -73,7 +76,23 @@ CollectionView::CollectionView(QWidget *parent) total_album_count_(-1), nomusic_(":/pictures/nomusic.png"), context_menu_(nullptr), - is_in_keyboard_search_(false) + action_load_(nullptr), + action_add_to_playlist_(nullptr), + action_add_to_playlist_enqueue_(nullptr), + action_add_to_playlist_enqueue_next_(nullptr), + action_open_in_new_playlist_(nullptr), + action_organize_(nullptr), +#ifndef Q_OS_WIN + action_copy_to_device_(nullptr), +#endif + action_edit_track_(nullptr), + action_edit_tracks_(nullptr), + action_rescan_songs_(nullptr), + action_show_in_browser_(nullptr), + action_show_in_various_(nullptr), + action_no_show_in_various_(nullptr), + is_in_keyboard_search_(false), + delete_files_(false) { setItemDelegate(new CollectionItemDelegate(this)); @@ -211,6 +230,8 @@ void CollectionView::ReloadSettings() { app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool()); } + delete_files_ = settings.value("delete_files", false).toBool(); + settings.endGroup(); } @@ -316,41 +337,41 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) { if (!context_menu_) { context_menu_ = new QMenu(this); - add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddToPlaylist())); - load_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(Load())); - open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist())); + action_add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddToPlaylist())); + action_load_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Replace current playlist"), this, SLOT(Load())); + action_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist())); context_menu_->addSeparator(); - add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue())); - add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, SLOT(AddToPlaylistEnqueueNext())); + action_add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue())); + action_add_to_playlist_enqueue_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, SLOT(AddToPlaylistEnqueueNext())); context_menu_->addSeparator(); - organize_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, SLOT(Organize())); + action_organize_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, SLOT(Organize())); #ifndef Q_OS_WIN - copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(CopyToDevice())); + action_copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(CopyToDevice())); #endif - //delete_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(Delete())); + action_delete_files_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(Delete())); context_menu_->addSeparator(); - edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks())); - edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, SLOT(EditTracks())); - show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser())); + action_edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks())); + action_edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, SLOT(EditTracks())); + action_show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser())); context_menu_->addSeparator(); - rescan_songs_ = context_menu_->addAction(tr("Rescan song(s)"), this, SLOT(RescanSongs())); + action_rescan_songs_ = context_menu_->addAction(tr("Rescan song(s)"), this, SLOT(RescanSongs())); context_menu_->addSeparator(); - show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious())); - no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious())); + action_show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious())); + action_no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious())); context_menu_->addSeparator(); context_menu_->addMenu(filter_->menu()); #ifndef Q_OS_WIN - copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0); - connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), copy_to_device_, SLOT(setDisabled(bool))); + action_copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0); + connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), action_copy_to_device_, SLOT(setDisabled(bool))); #endif } @@ -376,34 +397,45 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) { const bool regular_elements_only = songs_selected == regular_elements && regular_elements > 0; // in all modes - load_->setEnabled(songs_selected > 0); - add_to_playlist_->setEnabled(songs_selected > 0); - open_in_new_playlist_->setEnabled(songs_selected > 0); - add_to_playlist_enqueue_->setEnabled(songs_selected > 0); + action_load_->setEnabled(songs_selected > 0); + action_add_to_playlist_->setEnabled(songs_selected > 0); + action_open_in_new_playlist_->setEnabled(songs_selected > 0); + action_add_to_playlist_enqueue_->setEnabled(songs_selected > 0); // if neither edit_track not edit_tracks are available, we show disabled edit_track element - edit_track_->setVisible(regular_editable == 1); - edit_track_->setEnabled(regular_editable == 1); - edit_tracks_->setVisible(regular_editable > 1); - edit_tracks_->setEnabled(regular_editable > 1); + action_edit_track_->setVisible(regular_editable == 1); + action_edit_track_->setEnabled(regular_editable == 1); + action_edit_tracks_->setVisible(regular_editable > 1); + action_edit_tracks_->setEnabled(regular_editable > 1); - rescan_songs_->setVisible(regular_editable > 0); - rescan_songs_->setEnabled(regular_editable > 0); + action_rescan_songs_->setVisible(regular_editable > 0); + action_rescan_songs_->setEnabled(regular_editable > 0); - organize_->setVisible(regular_elements == regular_editable); + action_organize_->setVisible(regular_elements == regular_editable); #ifndef Q_OS_WIN - copy_to_device_->setVisible(regular_elements == regular_editable); + action_copy_to_device_->setVisible(regular_elements == regular_editable); #endif - //delete_->setVisible(regular_elements_only); - show_in_various_->setVisible(regular_elements_only); - no_show_in_various_->setVisible(regular_elements_only); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + action_delete_files_->setVisible(regular_elements == regular_editable && delete_files_); +#else + action_delete_files_->setVisible(false); +#endif + + action_show_in_various_->setVisible(regular_elements_only); + action_no_show_in_various_->setVisible(regular_elements_only); // only when all selected items are editable - organize_->setEnabled(regular_elements == regular_editable); + action_organize_->setEnabled(regular_elements == regular_editable); #ifndef Q_OS_WIN - copy_to_device_->setEnabled(regular_elements == regular_editable); + action_copy_to_device_->setEnabled(regular_elements == regular_editable); +#endif + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + action_delete_files_->setEnabled(regular_elements == regular_editable && delete_files_); +#else + action_delete_files_->setEnabled(false); #endif - //delete_->setEnabled(regular_elements == regular_editable); context_menu_->popup(e->globalPos()); @@ -619,3 +651,33 @@ int CollectionView::TotalArtists() { int CollectionView::TotalAlbums() { return total_album_count_; } + +void CollectionView::Delete() { + + if (!delete_files_) return; + + SongList selected_songs = GetSelectedSongs(); + QStringList files; + for (const Song &song : selected_songs) { + files << song.url().toString(); + } + if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return; + + // We can cheat and always take the storage of the first directory, since they'll all be FilesystemMusicStorage in a collection and deleting doesn't check the actual directory. + std::shared_ptr storage = app_->collection_model()->directory_model()->index(0, 0).data(MusicStorage::Role_Storage).value>(); + + DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage, true); + connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList))); + delete_files->Start(selected_songs); + +} + +void CollectionView::DeleteFilesFinished(const SongList &songs_with_errors) { + + if (songs_with_errors.isEmpty()) return; + + OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this); + dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors); + // It deletes itself when the user closes it + +} diff --git a/src/collection/collectionview.h b/src/collection/collectionview.h index eb3319b3f..77d9a040d 100644 --- a/src/collection/collectionview.h +++ b/src/collection/collectionview.h @@ -109,6 +109,8 @@ class CollectionView : public AutoExpandingTreeView { void ShowInBrowser(); void ShowInVarious(); void NoShowInVarious(); + void Delete(); + void DeleteFilesFinished(const SongList &songs_with_errors); private: void RecheckIsEmpty(); @@ -128,27 +130,28 @@ class CollectionView : public AutoExpandingTreeView { QMenu *context_menu_; QModelIndex context_menu_index_; - QAction *load_; - QAction *add_to_playlist_; - QAction *add_to_playlist_enqueue_; - QAction *add_to_playlist_enqueue_next_; - QAction *open_in_new_playlist_; - QAction *organize_; + QAction *action_load_; + QAction *action_add_to_playlist_; + QAction *action_add_to_playlist_enqueue_; + QAction *action_add_to_playlist_enqueue_next_; + QAction *action_open_in_new_playlist_; + QAction *action_organize_; #ifndef Q_OS_WIN - QAction *copy_to_device_; + QAction *action_copy_to_device_; #endif - QAction *delete_; - QAction *edit_track_; - QAction *edit_tracks_; - QAction *rescan_songs_; - QAction *show_in_browser_; - QAction *show_in_various_; - QAction *no_show_in_various_; + QAction *action_edit_track_; + QAction *action_edit_tracks_; + QAction *action_rescan_songs_; + QAction *action_show_in_browser_; + QAction *action_show_in_various_; + QAction *action_no_show_in_various_; + QAction *action_delete_files_; std::unique_ptr organize_dialog_; std::unique_ptr edit_tag_dialog_; bool is_in_keyboard_search_; + bool delete_files_; // Save focus Song last_selected_song_; diff --git a/src/core/deletefiles.cpp b/src/core/deletefiles.cpp index cbe3a60ea..85adf2ec5 100644 --- a/src/core/deletefiles.cpp +++ b/src/core/deletefiles.cpp @@ -34,10 +34,11 @@ const int DeleteFiles::kBatchSize = 50; -DeleteFiles::DeleteFiles(TaskManager *task_manager, std::shared_ptr storage) +DeleteFiles::DeleteFiles(TaskManager *task_manager, std::shared_ptr storage, const bool use_trash) : thread_(nullptr), task_manager_(task_manager), storage_(storage), + use_trash_(use_trash), started_(false), task_id_(0), progress_(0) { @@ -112,6 +113,7 @@ void DeleteFiles::ProcessSomeFiles() { MusicStorage::DeleteJob job; job.metadata_ = song; + job.use_trash_ = use_trash_; if (!storage_->DeleteFromStorage(job)) { songs_with_errors_ << song; diff --git a/src/core/deletefiles.h b/src/core/deletefiles.h index c73195551..5fb7288c4 100644 --- a/src/core/deletefiles.h +++ b/src/core/deletefiles.h @@ -38,7 +38,7 @@ class DeleteFiles : public QObject { Q_OBJECT public: - explicit DeleteFiles(TaskManager *task_manager, std::shared_ptr storage); + explicit DeleteFiles(TaskManager *task_manager, std::shared_ptr storage, const bool use_trash); ~DeleteFiles() override; static const int kBatchSize; @@ -59,6 +59,7 @@ signals: std::shared_ptr storage_; SongList songs_; + bool use_trash_; bool started_; diff --git a/src/core/filesystemmusicstorage.cpp b/src/core/filesystemmusicstorage.cpp index d6532627c..575ce4e7e 100644 --- a/src/core/filesystemmusicstorage.cpp +++ b/src/core/filesystemmusicstorage.cpp @@ -108,6 +108,17 @@ bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) { QString path = job.metadata_.url().toLocalFile(); QFileInfo fileInfo(path); + if (job.use_trash_) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + if (fileInfo.isDir()) + return Utilities::MoveToTrashRecursive(path); + else + return QFile::moveToTrash(path); +#else + return false; +#endif + } + if (fileInfo.isDir()) return Utilities::RemoveRecursive(path); else diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index a327bdb57..8baa50294 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -91,6 +91,8 @@ #include "database.h" #include "player.h" #include "appearance.h" +#include "filesystemmusicstorage.h" +#include "deletefiles.h" #include "engine/enginetype.h" #include "engine/enginebase.h" #include "engine/engine_fwd.h" @@ -100,6 +102,7 @@ #include "dialogs/trackselectiondialog.h" #include "dialogs/edittagdialog.h" #include "dialogs/addstreamdialog.h" +#include "dialogs/deleteconfirmationdialog.h" #include "organize/organizedialog.h" #include "widgets/fancytabwidget.h" #include "widgets/playingwidget.h" @@ -261,15 +264,16 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd playlist_play_pause_(nullptr), playlist_stop_after_(nullptr), playlist_undoredo_(nullptr), - playlist_organize_(nullptr), + playlist_copy_url_(nullptr), playlist_show_in_collection_(nullptr), playlist_copy_to_collection_(nullptr), playlist_move_to_collection_(nullptr), + playlist_open_in_browser_(nullptr), + playlist_organize_(nullptr), #ifndef Q_OS_WIN playlist_copy_to_device_(nullptr), #endif - playlist_open_in_browser_(nullptr), - playlist_copy_url_(nullptr), + playlist_delete_(nullptr), playlist_queue_(nullptr), playlist_queue_play_next_(nullptr), playlist_skip_(nullptr), @@ -286,7 +290,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour_Append), doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour_Never), menu_playmode_(BehaviourSettingsPage::PlayBehaviour_Never), - exit_count_(0) + exit_count_(0), + delete_files_(false) { qLog(Debug) << "Starting"; @@ -659,16 +664,16 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd playlist_menu_->addAction(ui_->action_add_files_to_transcoder); #endif playlist_menu_->addSeparator(); + playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Copy URL(s)..."), this, SLOT(PlaylistCopyUrl())); + playlist_show_in_collection_ = playlist_menu_->addAction(IconLoader::Load("edit-find"), tr("Show in collection..."), this, SLOT(ShowInCollection())); + playlist_open_in_browser_ = playlist_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(PlaylistOpenInBrowser())); + playlist_organize_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, SLOT(PlaylistMoveToCollection())); + playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Copy to collection..."), this, SLOT(PlaylistCopyToCollection())); + playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load("go-jump"), tr("Move to collection..."), this, SLOT(PlaylistMoveToCollection())); #if defined(HAVE_GSTREAMER) && !defined(Q_OS_WIN) playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(PlaylistCopyToDevice())); #endif - playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Copy to collection..."), this, SLOT(PlaylistCopyToCollection())); - playlist_move_to_collection_ = playlist_menu_->addAction(IconLoader::Load("go-jump"), tr("Move to collection..."), this, SLOT(PlaylistMoveToCollection())); - playlist_organize_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organize files..."), this, SLOT(PlaylistMoveToCollection())); - playlist_open_in_browser_ = playlist_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(PlaylistOpenInBrowser())); - playlist_open_in_browser_->setVisible(false); - playlist_show_in_collection_ = playlist_menu_->addAction(IconLoader::Load("edit-find"), tr("Show in collection..."), this, SLOT(ShowInCollection())); - playlist_copy_url_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Copy URL(s)..."), this, SLOT(PlaylistCopyUrl())); + playlist_delete_ = playlist_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(PlaylistDelete())); playlist_menu_->addSeparator(); playlistitem_actions_separator_ = playlist_menu_->addSeparator(); playlist_menu_->addAction(ui_->action_clear_playlist); @@ -990,6 +995,10 @@ void MainWindow::ReloadSettings() { } } + s.beginGroup(PlaylistSettingsPage::kSettingsGroup); + delete_files_ = s.value("delete_files", false).toBool(); + s.endGroup(); + osd_->ReloadSettings(); album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings_.value("search_for_cover_auto", true).toBool()); @@ -1733,6 +1742,7 @@ void MainWindow::PlaylistRightClick(const QPoint &global_pos, const QModelIndex playlist_copy_to_device_->setVisible(false); #endif playlist_organize_->setVisible(false); + playlist_delete_->setVisible(false); playlist_copy_url_->setVisible(selected > 0); @@ -1805,6 +1815,10 @@ void MainWindow::PlaylistRightClick(const QPoint &global_pos, const QModelIndex playlist_copy_to_device_->setVisible(editable > 0); #endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + playlist_delete_->setVisible(delete_files_ && editable > 0); +#endif + // Remove old item actions, if any. for (QAction *action : playlistitem_actions_) { playlist_menu_->removeAction(action); @@ -2850,3 +2864,39 @@ void MainWindow::Love() { if (tray_icon_) tray_icon_->LoveStateChanged(false); } + +void MainWindow::PlaylistDelete() { + + if (!delete_files_) return; + + SongList selected_songs; + QStringList files; + bool is_current_item = false; + for (const QModelIndex &proxy_idx : ui_->playlist->view()->selectionModel()->selectedRows()) { + QModelIndex source_idx = app_->playlist_manager()->current()->proxy()->mapToSource(proxy_idx); + PlaylistItemPtr item = app_->playlist_manager()->current()->item_at(source_idx.row()); + if (!item || !item->Metadata().url().isLocalFile()) continue; + selected_songs << item->Metadata(); + files << item->Metadata().url().toLocalFile(); + if (item == app_->player()->GetCurrentItem()) is_current_item = true; + } + if (selected_songs.isEmpty()) return; + + if (DeleteConfirmationDialog::warning(files) != QDialogButtonBox::Yes) return; + + if (app_->player()->GetState() == Engine::Playing && app_->playlist_manager()->current()->rowCount() == selected_songs.count()) { + app_->player()->Stop(); + } + + ui_->playlist->view()->RemoveSelected(); + + if (app_->player()->GetState() == Engine::Playing && is_current_item) { + app_->player()->Next(); + } + + std::shared_ptr storage(new FilesystemMusicStorage("/")); + DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage, true); + connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList))); + delete_files->Start(selected_songs); + +} diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 3552ae7b2..99bc2e604 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -264,6 +264,8 @@ class MainWindow : public QMainWindow, public PlatformInterface { void ExitFinished(); + void PlaylistDelete(); + private: void SaveSettings(); @@ -331,15 +333,16 @@ class MainWindow : public QMainWindow, public PlatformInterface { QAction *playlist_play_pause_; QAction *playlist_stop_after_; QAction *playlist_undoredo_; - QAction *playlist_organize_; + QAction *playlist_copy_url_; QAction *playlist_show_in_collection_; QAction *playlist_copy_to_collection_; QAction *playlist_move_to_collection_; + QAction *playlist_open_in_browser_; + QAction *playlist_organize_; #ifndef Q_OS_WIN QAction *playlist_copy_to_device_; #endif - QAction *playlist_open_in_browser_; - QAction *playlist_copy_url_; + QAction *playlist_delete_; QAction *playlist_queue_; QAction* playlist_queue_play_next_; QAction *playlist_skip_; @@ -369,6 +372,7 @@ class MainWindow : public QMainWindow, public PlatformInterface { Song song_playing_; QImage image_original_; int exit_count_; + bool delete_files_; }; diff --git a/src/core/musicstorage.h b/src/core/musicstorage.h index d4043803d..099bff73d 100644 --- a/src/core/musicstorage.h +++ b/src/core/musicstorage.h @@ -56,6 +56,7 @@ class MusicStorage { typedef std::function ProgressFunction; struct CopyJob { + CopyJob() : overwrite_(false), mark_as_listened_(false), remove_original_(false), albumcover_(false) {} QString source_; QString destination_; Song metadata_; @@ -70,7 +71,9 @@ class MusicStorage { }; struct DeleteJob { + DeleteJob() : use_trash_(false) {} Song metadata_; + bool use_trash_; }; virtual QString LocalPath() const { return QString(); } diff --git a/src/core/utilities.cpp b/src/core/utilities.cpp index 46b91b8d4..bbe37a86c 100644 --- a/src/core/utilities.cpp +++ b/src/core/utilities.cpp @@ -253,6 +253,30 @@ QString MakeTempDir(const QString template_name) { } +bool MoveToTrashRecursive(const QString &path) { + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QDir dir(path); + for (const QString &child : dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Hidden)) { + if (!MoveToTrashRecursive(path + "/" + child)) + return false; + } + + for (const QString &child : dir.entryList(QDir::NoDotAndDotDot | QDir::Files | QDir::Hidden)) { + if (!QFile::moveToTrash(path + "/" + child)) + return false; + } + + return dir.rmdir(path); + +#else + + return false; + +#endif + +} + bool RemoveRecursive(const QString &path) { QDir dir(path); diff --git a/src/core/utilities.h b/src/core/utilities.h index 717783d6a..c79bce9f2 100644 --- a/src/core/utilities.h +++ b/src/core/utilities.h @@ -64,6 +64,7 @@ quint64 FileSystemFreeSpace(const QString &path); QString MakeTempDir(const QString template_name = QString()); +bool MoveToTrashRecursive(const QString &path); bool RemoveRecursive(const QString &path); bool CopyRecursive(const QString &source, const QString &destination); bool Copy(QIODevice *source, QIODevice *destination); diff --git a/src/device/deviceview.cpp b/src/device/deviceview.cpp index c4247e06c..5d787d99f 100644 --- a/src/device/deviceview.cpp +++ b/src/device/deviceview.cpp @@ -421,7 +421,7 @@ void DeviceView::Delete() { std::shared_ptr storage = device_index.data(MusicStorage::Role_Storage).value>(); - DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage); + DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage, false); connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList))); delete_files->Start(GetSelectedSongs()); diff --git a/src/dialogs/deleteconfirmationdialog.cpp b/src/dialogs/deleteconfirmationdialog.cpp new file mode 100644 index 000000000..20a9a51a9 --- /dev/null +++ b/src/dialogs/deleteconfirmationdialog.cpp @@ -0,0 +1,114 @@ +/* + * Strawberry Music Player + * Copyright 2020, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "deleteconfirmationdialog.h" + +DeleteConfirmationDialog::DeleteConfirmationDialog(const QStringList &files, QWidget *parent) : QDialog(parent, Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint), button_box_(new QDialogButtonBox(this)) { + + setModal(true); + setWindowTitle(tr("Delete files")); + setWindowIcon(style()->standardIcon(QStyle::SP_MessageBoxWarning, 0, this)); + + QLabel *label_icon = new QLabel(this); + label_icon->setPixmap(style()->standardIcon(QStyle::SP_MessageBoxWarning, 0, this).pixmap(style()->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, this), style()->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, this))); + label_icon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + QLabel *label_text_top = new QLabel(this); + QFont label_text_top_font = label_text_top->font(); + label_text_top_font.setBold(true); + label_text_top_font.setPointSize(label_text_top_font.pointSize() + 4); + label_text_top->setTextInteractionFlags(Qt::TextInteractionFlags(style()->styleHint(QStyle::SH_MessageBox_TextInteractionFlags, 0, this))); + label_text_top->setContentsMargins(0, 0, 0, 0); + label_text_top->setFont(label_text_top_font); + label_text_top->setText(tr("The following files will be deleted from disk:")); + + QListWidget *list = new QListWidget(this); + list->setSelectionMode(QAbstractItemView::NoSelection); + list->addItems(files); + + QLabel *label_text_bottom = new QLabel(this); + QFont label_text_bottom_font = label_text_bottom->font(); + label_text_bottom_font.setBold(true); + label_text_bottom_font.setPointSize(label_text_bottom_font.pointSize() + 4); + label_text_bottom->setTextInteractionFlags(Qt::TextInteractionFlags(style()->styleHint(QStyle::SH_MessageBox_TextInteractionFlags, 0, this))); + label_text_bottom->setContentsMargins(0, 0, 0, 0); + label_text_bottom->setFont(label_text_bottom_font); + label_text_bottom->setText(tr("Are you sure you want to continue?")); + + button_box_->setStandardButtons(QDialogButtonBox::Yes|QDialogButtonBox::Cancel); + connect(button_box_, SIGNAL(clicked(QAbstractButton*)), this, SLOT(ButtonClicked(QAbstractButton*))); + + // Add layout + QGridLayout *grid = new QGridLayout(this); + grid->addWidget(label_icon, 0, 0, 2, 1, Qt::AlignTop); + grid->addWidget(label_text_top, 0, 1, 1, 1); + grid->addWidget(list, 1, 1, 1, 2); + grid->addWidget(label_text_bottom, 2, 1, 1, 2); + grid->addWidget(button_box_, 3, 1, 1, 2, Qt::AlignRight); + grid->setSizeConstraint(QLayout::SetNoConstraint); + setLayout(grid); + + // Set size of dialog + int max_width = 0; + int max_height = 0; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + QScreen *screen = QWidget::screen(); +#else + QScreen *screen = (window() && window()->windowHandle() ? window()->windowHandle()->screen() : QGuiApplication::primaryScreen()); +#endif + if (screen) { + max_width = screen->geometry().size().width() / 0.5; + max_height = static_cast(float(screen->geometry().size().height()) / float(1.5)); + } + int min_width = std::min(list->sizeHintForColumn(0) + 100, max_width); + int min_height = std::min((list->sizeHintForRow(0) * list->count()) + 160, max_height); + setMinimumSize(min_width, min_height); + adjustSize(); + setMinimumSize(0, 0); + +} + +void DeleteConfirmationDialog::ButtonClicked(QAbstractButton *button) { + + done(button_box_->standardButton(button)); + +} + +QDialogButtonBox::StandardButton DeleteConfirmationDialog::warning(const QStringList &files, QWidget *parent) { + + DeleteConfirmationDialog box(files, parent); + return static_cast(box.exec()); + +} diff --git a/src/dialogs/deleteconfirmationdialog.h b/src/dialogs/deleteconfirmationdialog.h new file mode 100644 index 000000000..143011b04 --- /dev/null +++ b/src/dialogs/deleteconfirmationdialog.h @@ -0,0 +1,43 @@ +/* + * Strawberry Music Player + * Copyright 2020, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef DELETECONFIRMATIONDIALOG_H +#define DELETECONFIRMATIONDIALOG_H + +#include +#include +#include + +class DeleteConfirmationDialog : public QDialog { + Q_OBJECT + + public: + DeleteConfirmationDialog(const QStringList &files, QWidget *parent = nullptr); + + static QDialogButtonBox::StandardButton warning(const QStringList &files, QWidget *parent = nullptr); + + private slots: + void ButtonClicked(QAbstractButton *button); + + private: + QDialogButtonBox *button_box_; + +}; + +#endif // DELETECONFIRMATIONDIALOG_H diff --git a/src/settings/collectionsettingspage.cpp b/src/settings/collectionsettingspage.cpp index 3e490a743..5acb42557 100644 --- a/src/settings/collectionsettingspage.cpp +++ b/src/settings/collectionsettingspage.cpp @@ -178,6 +178,8 @@ void CollectionSettingsPage::Load() { ui_->spinbox_disk_cache_size->setValue(s.value(kSettingsDiskCacheSize, kSettingsDiskCacheSizeDefault).toInt()); ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, static_cast(CacheSizeUnit_MB)).toInt()); + ui_->checkbox_delete_files->setChecked(s.value("delete_files", false).toBool()); + s.endGroup(); DiskCacheEnable(ui_->checkbox_disk_cache->checkState()); @@ -227,6 +229,8 @@ void CollectionSettingsPage::Save() { s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value()); s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex()); + s.setValue("delete_files", ui_->checkbox_delete_files->isChecked()); + s.endGroup(); } diff --git a/src/settings/collectionsettingspage.ui b/src/settings/collectionsettingspage.ui index 93742081a..1580982a8 100644 --- a/src/settings/collectionsettingspage.ui +++ b/src/settings/collectionsettingspage.ui @@ -450,6 +450,13 @@ If there are no matches then it will use the largest image in the directory. + + + + Enable delete files in the right click context menu + + + diff --git a/src/settings/playlistsettingspage.cpp b/src/settings/playlistsettingspage.cpp index 8fb64abd5..b0943bb8c 100644 --- a/src/settings/playlistsettingspage.cpp +++ b/src/settings/playlistsettingspage.cpp @@ -83,6 +83,8 @@ void PlaylistSettingsPage::Load() { ui_->checkbox_editmetadatainline->setChecked(s.value("editmetadatainline", false).toBool()); ui_->checkbox_writemetadata->setChecked(s.value(Playlist::kWriteMetadata, false).toBool()); + ui_->checkbox_delete_files->setChecked(s.value("delete_files", false).toBool()); + s.endGroup(); Init(ui_->layout_playlistsettingspage->parentWidget()); @@ -118,6 +120,7 @@ void PlaylistSettingsPage::Save() { s.setValue(Playlist::kPathType, static_cast(path)); s.setValue("editmetadatainline", ui_->checkbox_editmetadatainline->isChecked()); s.setValue(Playlist::kWriteMetadata, ui_->checkbox_writemetadata->isChecked()); + s.setValue("delete_files", ui_->checkbox_delete_files->isChecked()); s.endGroup(); } diff --git a/src/settings/playlistsettingspage.ui b/src/settings/playlistsettingspage.ui index 289ffa5a6..a677d4ad4 100644 --- a/src/settings/playlistsettingspage.ui +++ b/src/settings/playlistsettingspage.ui @@ -63,6 +63,13 @@ + + + + Enable delete files in the right click context menu + + + diff --git a/src/widgets/fileview.cpp b/src/widgets/fileview.cpp index 6bc9da5e2..23563fe3f 100644 --- a/src/widgets/fileview.cpp +++ b/src/widgets/fileview.cpp @@ -39,6 +39,7 @@ #include "core/filesystemmusicstorage.h" #include "core/iconloader.h" #include "core/mimedata.h" +#include "dialogs/deleteconfirmationdialog.h" #include "fileview.h" #include "fileviewlist.h" #include "ui_fileview.h" @@ -85,9 +86,7 @@ FileView::FileView(QWidget *parent) connect(ui_->list, SIGNAL(CopyToCollection(QList)), SIGNAL(CopyToCollection(QList))); connect(ui_->list, SIGNAL(MoveToCollection(QList)), SIGNAL(MoveToCollection(QList))); connect(ui_->list, SIGNAL(CopyToDevice(QList)), SIGNAL(CopyToDevice(QList))); -#ifdef HAVE_GSTREAMER connect(ui_->list, SIGNAL(Delete(QStringList)), SLOT(Delete(QStringList))); -#endif connect(ui_->list, SIGNAL(EditTags(QList)), SIGNAL(EditTags(QList))); QString filter(FileView::kFileFilter); @@ -231,40 +230,24 @@ void FileView::UndoCommand::undo() { void FileView::Delete(const QStringList &filenames) { -#ifdef HAVE_GSTREAMER + if (filenames.isEmpty()) return; - if (filenames.isEmpty()) - return; + if (DeleteConfirmationDialog::warning(filenames) != QDialogButtonBox::Yes) return; - if (QMessageBox::warning(this, tr("Delete files"), - tr("These files will be deleted from disk, are you sure you want to continue?"), - QMessageBox::Yes, QMessageBox::Cancel) != QMessageBox::Yes) - return; - - DeleteFiles *delete_files = new DeleteFiles(task_manager_, storage_); + DeleteFiles *delete_files = new DeleteFiles(task_manager_, storage_, true); connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList))); delete_files->Start(filenames); -#else - Q_UNUSED(filenames) -#endif - } void FileView::DeleteFinished(const SongList &songs_with_errors) { -#ifdef HAVE_GSTREAMER - if (songs_with_errors.isEmpty()) return; OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this); dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors); // It deletes itself when the user closes it -#else - Q_UNUSED(songs_with_errors) -#endif - } void FileView::showEvent(QShowEvent *e) {