Add optional delete from disk in collection and playlist

Fixes #284
This commit is contained in:
Jonas Kvinge 2020-08-19 22:02:35 +02:00
parent 9b14df6b27
commit 653a35496d
19 changed files with 411 additions and 87 deletions

View File

@ -169,6 +169,7 @@ set(SOURCES
dialogs/trackselectiondialog.cpp dialogs/trackselectiondialog.cpp
dialogs/addstreamdialog.cpp dialogs/addstreamdialog.cpp
dialogs/userpassdialog.cpp dialogs/userpassdialog.cpp
dialogs/deleteconfirmationdialog.cpp
widgets/autoexpandingtreeview.cpp widgets/autoexpandingtreeview.cpp
widgets/busyindicator.cpp widgets/busyindicator.cpp
@ -367,6 +368,7 @@ set(HEADERS
dialogs/trackselectiondialog.h dialogs/trackselectiondialog.h
dialogs/addstreamdialog.h dialogs/addstreamdialog.h
dialogs/userpassdialog.h dialogs/userpassdialog.h
dialogs/deleteconfirmationdialog.h
widgets/autoexpandingtreeview.h widgets/autoexpandingtreeview.h
widgets/busyindicator.h widgets/busyindicator.h

View File

@ -48,6 +48,7 @@
#include "core/iconloader.h" #include "core/iconloader.h"
#include "core/mimedata.h" #include "core/mimedata.h"
#include "core/utilities.h" #include "core/utilities.h"
#include "core/deletefiles.h"
#include "collection.h" #include "collection.h"
#include "collectionbackend.h" #include "collectionbackend.h"
#include "collectiondirectorymodel.h" #include "collectiondirectorymodel.h"
@ -61,7 +62,9 @@
# include "device/devicestatefiltermodel.h" # include "device/devicestatefiltermodel.h"
#endif #endif
#include "dialogs/edittagdialog.h" #include "dialogs/edittagdialog.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "organize/organizedialog.h" #include "organize/organizedialog.h"
#include "organize/organizeerrordialog.h"
#include "settings/collectionsettingspage.h" #include "settings/collectionsettingspage.h"
CollectionView::CollectionView(QWidget *parent) CollectionView::CollectionView(QWidget *parent)
@ -73,7 +76,23 @@ CollectionView::CollectionView(QWidget *parent)
total_album_count_(-1), total_album_count_(-1),
nomusic_(":/pictures/nomusic.png"), nomusic_(":/pictures/nomusic.png"),
context_menu_(nullptr), 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)); setItemDelegate(new CollectionItemDelegate(this));
@ -211,6 +230,8 @@ void CollectionView::ReloadSettings() {
app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool()); app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool());
} }
delete_files_ = settings.value("delete_files", false).toBool();
settings.endGroup(); settings.endGroup();
} }
@ -316,41 +337,41 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
if (!context_menu_) { if (!context_menu_) {
context_menu_ = new QMenu(this); context_menu_ = new QMenu(this);
add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-playback-start"), tr("Append to current playlist"), this, SLOT(AddToPlaylist())); action_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())); action_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_open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
context_menu_->addSeparator(); context_menu_->addSeparator();
add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue())); action_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_next_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue to play next"), this, SLOT(AddToPlaylistEnqueueNext()));
context_menu_->addSeparator(); 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 #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 #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(); context_menu_->addSeparator();
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks())); action_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())); action_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_show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
context_menu_->addSeparator(); 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(); context_menu_->addSeparator();
show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious())); action_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_no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious()));
context_menu_->addSeparator(); context_menu_->addSeparator();
context_menu_->addMenu(filter_->menu()); context_menu_->addMenu(filter_->menu());
#ifndef Q_OS_WIN #ifndef Q_OS_WIN
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0); action_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))); connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), action_copy_to_device_, SLOT(setDisabled(bool)));
#endif #endif
} }
@ -376,34 +397,45 @@ void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
const bool regular_elements_only = songs_selected == regular_elements && regular_elements > 0; const bool regular_elements_only = songs_selected == regular_elements && regular_elements > 0;
// in all modes // in all modes
load_->setEnabled(songs_selected > 0); action_load_->setEnabled(songs_selected > 0);
add_to_playlist_->setEnabled(songs_selected > 0); action_add_to_playlist_->setEnabled(songs_selected > 0);
open_in_new_playlist_->setEnabled(songs_selected > 0); action_open_in_new_playlist_->setEnabled(songs_selected > 0);
add_to_playlist_enqueue_->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 // if neither edit_track not edit_tracks are available, we show disabled edit_track element
edit_track_->setVisible(regular_editable == 1); action_edit_track_->setVisible(regular_editable == 1);
edit_track_->setEnabled(regular_editable == 1); action_edit_track_->setEnabled(regular_editable == 1);
edit_tracks_->setVisible(regular_editable > 1); action_edit_tracks_->setVisible(regular_editable > 1);
edit_tracks_->setEnabled(regular_editable > 1); action_edit_tracks_->setEnabled(regular_editable > 1);
rescan_songs_->setVisible(regular_editable > 0); action_rescan_songs_->setVisible(regular_editable > 0);
rescan_songs_->setEnabled(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 #ifndef Q_OS_WIN
copy_to_device_->setVisible(regular_elements == regular_editable); action_copy_to_device_->setVisible(regular_elements == regular_editable);
#endif #endif
//delete_->setVisible(regular_elements_only);
show_in_various_->setVisible(regular_elements_only); #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
no_show_in_various_->setVisible(regular_elements_only); 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 // only when all selected items are editable
organize_->setEnabled(regular_elements == regular_editable); action_organize_->setEnabled(regular_elements == regular_editable);
#ifndef Q_OS_WIN #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 #endif
//delete_->setEnabled(regular_elements == regular_editable);
context_menu_->popup(e->globalPos()); context_menu_->popup(e->globalPos());
@ -619,3 +651,33 @@ int CollectionView::TotalArtists() {
int CollectionView::TotalAlbums() { int CollectionView::TotalAlbums() {
return total_album_count_; 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<MusicStorage> storage = app_->collection_model()->directory_model()->index(0, 0).data(MusicStorage::Role_Storage).value<std::shared_ptr<MusicStorage>>();
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
}

View File

@ -109,6 +109,8 @@ class CollectionView : public AutoExpandingTreeView {
void ShowInBrowser(); void ShowInBrowser();
void ShowInVarious(); void ShowInVarious();
void NoShowInVarious(); void NoShowInVarious();
void Delete();
void DeleteFilesFinished(const SongList &songs_with_errors);
private: private:
void RecheckIsEmpty(); void RecheckIsEmpty();
@ -128,27 +130,28 @@ class CollectionView : public AutoExpandingTreeView {
QMenu *context_menu_; QMenu *context_menu_;
QModelIndex context_menu_index_; QModelIndex context_menu_index_;
QAction *load_; QAction *action_load_;
QAction *add_to_playlist_; QAction *action_add_to_playlist_;
QAction *add_to_playlist_enqueue_; QAction *action_add_to_playlist_enqueue_;
QAction *add_to_playlist_enqueue_next_; QAction *action_add_to_playlist_enqueue_next_;
QAction *open_in_new_playlist_; QAction *action_open_in_new_playlist_;
QAction *organize_; QAction *action_organize_;
#ifndef Q_OS_WIN #ifndef Q_OS_WIN
QAction *copy_to_device_; QAction *action_copy_to_device_;
#endif #endif
QAction *delete_; QAction *action_edit_track_;
QAction *edit_track_; QAction *action_edit_tracks_;
QAction *edit_tracks_; QAction *action_rescan_songs_;
QAction *rescan_songs_; QAction *action_show_in_browser_;
QAction *show_in_browser_; QAction *action_show_in_various_;
QAction *show_in_various_; QAction *action_no_show_in_various_;
QAction *no_show_in_various_; QAction *action_delete_files_;
std::unique_ptr<OrganizeDialog> organize_dialog_; std::unique_ptr<OrganizeDialog> organize_dialog_;
std::unique_ptr<EditTagDialog> edit_tag_dialog_; std::unique_ptr<EditTagDialog> edit_tag_dialog_;
bool is_in_keyboard_search_; bool is_in_keyboard_search_;
bool delete_files_;
// Save focus // Save focus
Song last_selected_song_; Song last_selected_song_;

View File

@ -34,10 +34,11 @@
const int DeleteFiles::kBatchSize = 50; const int DeleteFiles::kBatchSize = 50;
DeleteFiles::DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage) DeleteFiles::DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage, const bool use_trash)
: thread_(nullptr), : thread_(nullptr),
task_manager_(task_manager), task_manager_(task_manager),
storage_(storage), storage_(storage),
use_trash_(use_trash),
started_(false), started_(false),
task_id_(0), task_id_(0),
progress_(0) { progress_(0) {
@ -112,6 +113,7 @@ void DeleteFiles::ProcessSomeFiles() {
MusicStorage::DeleteJob job; MusicStorage::DeleteJob job;
job.metadata_ = song; job.metadata_ = song;
job.use_trash_ = use_trash_;
if (!storage_->DeleteFromStorage(job)) { if (!storage_->DeleteFromStorage(job)) {
songs_with_errors_ << song; songs_with_errors_ << song;

View File

@ -38,7 +38,7 @@ class DeleteFiles : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage); explicit DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage, const bool use_trash);
~DeleteFiles() override; ~DeleteFiles() override;
static const int kBatchSize; static const int kBatchSize;
@ -59,6 +59,7 @@ signals:
std::shared_ptr<MusicStorage> storage_; std::shared_ptr<MusicStorage> storage_;
SongList songs_; SongList songs_;
bool use_trash_;
bool started_; bool started_;

View File

@ -108,6 +108,17 @@ bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
QString path = job.metadata_.url().toLocalFile(); QString path = job.metadata_.url().toLocalFile();
QFileInfo fileInfo(path); 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()) if (fileInfo.isDir())
return Utilities::RemoveRecursive(path); return Utilities::RemoveRecursive(path);
else else

View File

@ -91,6 +91,8 @@
#include "database.h" #include "database.h"
#include "player.h" #include "player.h"
#include "appearance.h" #include "appearance.h"
#include "filesystemmusicstorage.h"
#include "deletefiles.h"
#include "engine/enginetype.h" #include "engine/enginetype.h"
#include "engine/enginebase.h" #include "engine/enginebase.h"
#include "engine/engine_fwd.h" #include "engine/engine_fwd.h"
@ -100,6 +102,7 @@
#include "dialogs/trackselectiondialog.h" #include "dialogs/trackselectiondialog.h"
#include "dialogs/edittagdialog.h" #include "dialogs/edittagdialog.h"
#include "dialogs/addstreamdialog.h" #include "dialogs/addstreamdialog.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "organize/organizedialog.h" #include "organize/organizedialog.h"
#include "widgets/fancytabwidget.h" #include "widgets/fancytabwidget.h"
#include "widgets/playingwidget.h" #include "widgets/playingwidget.h"
@ -261,15 +264,16 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
playlist_play_pause_(nullptr), playlist_play_pause_(nullptr),
playlist_stop_after_(nullptr), playlist_stop_after_(nullptr),
playlist_undoredo_(nullptr), playlist_undoredo_(nullptr),
playlist_organize_(nullptr), playlist_copy_url_(nullptr),
playlist_show_in_collection_(nullptr), playlist_show_in_collection_(nullptr),
playlist_copy_to_collection_(nullptr), playlist_copy_to_collection_(nullptr),
playlist_move_to_collection_(nullptr), playlist_move_to_collection_(nullptr),
playlist_open_in_browser_(nullptr),
playlist_organize_(nullptr),
#ifndef Q_OS_WIN #ifndef Q_OS_WIN
playlist_copy_to_device_(nullptr), playlist_copy_to_device_(nullptr),
#endif #endif
playlist_open_in_browser_(nullptr), playlist_delete_(nullptr),
playlist_copy_url_(nullptr),
playlist_queue_(nullptr), playlist_queue_(nullptr),
playlist_queue_play_next_(nullptr), playlist_queue_play_next_(nullptr),
playlist_skip_(nullptr), playlist_skip_(nullptr),
@ -286,7 +290,8 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour_Append), doubleclick_addmode_(BehaviourSettingsPage::AddBehaviour_Append),
doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour_Never), doubleclick_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
menu_playmode_(BehaviourSettingsPage::PlayBehaviour_Never), menu_playmode_(BehaviourSettingsPage::PlayBehaviour_Never),
exit_count_(0) exit_count_(0),
delete_files_(false)
{ {
qLog(Debug) << "Starting"; 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); playlist_menu_->addAction(ui_->action_add_files_to_transcoder);
#endif #endif
playlist_menu_->addSeparator(); 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) #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())); playlist_copy_to_device_ = playlist_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(PlaylistCopyToDevice()));
#endif #endif
playlist_copy_to_collection_ = playlist_menu_->addAction(IconLoader::Load("edit-copy"), tr("Copy to collection..."), this, SLOT(PlaylistCopyToCollection())); playlist_delete_ = playlist_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(PlaylistDelete()));
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_menu_->addSeparator(); playlist_menu_->addSeparator();
playlistitem_actions_separator_ = playlist_menu_->addSeparator(); playlistitem_actions_separator_ = playlist_menu_->addSeparator();
playlist_menu_->addAction(ui_->action_clear_playlist); 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(); osd_->ReloadSettings();
album_cover_choice_controller_->search_cover_auto_action()->setChecked(settings_.value("search_for_cover_auto", true).toBool()); 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); playlist_copy_to_device_->setVisible(false);
#endif #endif
playlist_organize_->setVisible(false); playlist_organize_->setVisible(false);
playlist_delete_->setVisible(false);
playlist_copy_url_->setVisible(selected > 0); 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); playlist_copy_to_device_->setVisible(editable > 0);
#endif #endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
playlist_delete_->setVisible(delete_files_ && editable > 0);
#endif
// Remove old item actions, if any. // Remove old item actions, if any.
for (QAction *action : playlistitem_actions_) { for (QAction *action : playlistitem_actions_) {
playlist_menu_->removeAction(action); playlist_menu_->removeAction(action);
@ -2850,3 +2864,39 @@ void MainWindow::Love() {
if (tray_icon_) tray_icon_->LoveStateChanged(false); 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<MusicStorage> 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);
}

View File

@ -264,6 +264,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void ExitFinished(); void ExitFinished();
void PlaylistDelete();
private: private:
void SaveSettings(); void SaveSettings();
@ -331,15 +333,16 @@ class MainWindow : public QMainWindow, public PlatformInterface {
QAction *playlist_play_pause_; QAction *playlist_play_pause_;
QAction *playlist_stop_after_; QAction *playlist_stop_after_;
QAction *playlist_undoredo_; QAction *playlist_undoredo_;
QAction *playlist_organize_; QAction *playlist_copy_url_;
QAction *playlist_show_in_collection_; QAction *playlist_show_in_collection_;
QAction *playlist_copy_to_collection_; QAction *playlist_copy_to_collection_;
QAction *playlist_move_to_collection_; QAction *playlist_move_to_collection_;
QAction *playlist_open_in_browser_;
QAction *playlist_organize_;
#ifndef Q_OS_WIN #ifndef Q_OS_WIN
QAction *playlist_copy_to_device_; QAction *playlist_copy_to_device_;
#endif #endif
QAction *playlist_open_in_browser_; QAction *playlist_delete_;
QAction *playlist_copy_url_;
QAction *playlist_queue_; QAction *playlist_queue_;
QAction* playlist_queue_play_next_; QAction* playlist_queue_play_next_;
QAction *playlist_skip_; QAction *playlist_skip_;
@ -369,6 +372,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
Song song_playing_; Song song_playing_;
QImage image_original_; QImage image_original_;
int exit_count_; int exit_count_;
bool delete_files_;
}; };

View File

@ -56,6 +56,7 @@ class MusicStorage {
typedef std::function<void(float progress)> ProgressFunction; typedef std::function<void(float progress)> ProgressFunction;
struct CopyJob { struct CopyJob {
CopyJob() : overwrite_(false), mark_as_listened_(false), remove_original_(false), albumcover_(false) {}
QString source_; QString source_;
QString destination_; QString destination_;
Song metadata_; Song metadata_;
@ -70,7 +71,9 @@ class MusicStorage {
}; };
struct DeleteJob { struct DeleteJob {
DeleteJob() : use_trash_(false) {}
Song metadata_; Song metadata_;
bool use_trash_;
}; };
virtual QString LocalPath() const { return QString(); } virtual QString LocalPath() const { return QString(); }

View File

@ -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) { bool RemoveRecursive(const QString &path) {
QDir dir(path); QDir dir(path);

View File

@ -64,6 +64,7 @@ quint64 FileSystemFreeSpace(const QString &path);
QString MakeTempDir(const QString template_name = QString()); QString MakeTempDir(const QString template_name = QString());
bool MoveToTrashRecursive(const QString &path);
bool RemoveRecursive(const QString &path); bool RemoveRecursive(const QString &path);
bool CopyRecursive(const QString &source, const QString &destination); bool CopyRecursive(const QString &source, const QString &destination);
bool Copy(QIODevice *source, QIODevice *destination); bool Copy(QIODevice *source, QIODevice *destination);

View File

@ -421,7 +421,7 @@ void DeviceView::Delete() {
std::shared_ptr<MusicStorage> storage = device_index.data(MusicStorage::Role_Storage).value<std::shared_ptr<MusicStorage>>(); std::shared_ptr<MusicStorage> storage = device_index.data(MusicStorage::Role_Storage).value<std::shared_ptr<MusicStorage>>();
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))); connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList)));
delete_files->Start(GetSelectedSongs()); delete_files->Start(GetSelectedSongs());

View File

@ -0,0 +1,114 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <algorithm>
#include <QGuiApplication>
#include <QScreen>
#include <QStringList>
#include <QStyle>
#include <QFont>
#include <QDialogButtonBox>
#include <QAbstractButton>
#include <QPushButton>
#include <QGridLayout>
#include <QScrollArea>
#include <QAbstractItemView>
#include <QListWidget>
#include <QLabel>
#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<int>(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<QDialogButtonBox::StandardButton>(box.exec());
}

View File

@ -0,0 +1,43 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef DELETECONFIRMATIONDIALOG_H
#define DELETECONFIRMATIONDIALOG_H
#include <QDialog>
#include <QDialogButtonBox>
#include <QStringList>
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

View File

@ -178,6 +178,8 @@ void CollectionSettingsPage::Load() {
ui_->spinbox_disk_cache_size->setValue(s.value(kSettingsDiskCacheSize, kSettingsDiskCacheSizeDefault).toInt()); ui_->spinbox_disk_cache_size->setValue(s.value(kSettingsDiskCacheSize, kSettingsDiskCacheSizeDefault).toInt());
ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, static_cast<int>(CacheSizeUnit_MB)).toInt()); ui_->combobox_disk_cache_size->setCurrentIndex(s.value(kSettingsDiskCacheSizeUnit, static_cast<int>(CacheSizeUnit_MB)).toInt());
ui_->checkbox_delete_files->setChecked(s.value("delete_files", false).toBool());
s.endGroup(); s.endGroup();
DiskCacheEnable(ui_->checkbox_disk_cache->checkState()); DiskCacheEnable(ui_->checkbox_disk_cache->checkState());
@ -227,6 +229,8 @@ void CollectionSettingsPage::Save() {
s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value()); s.setValue(kSettingsDiskCacheSize, ui_->spinbox_disk_cache_size->value());
s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex()); s.setValue(kSettingsDiskCacheSizeUnit, ui_->combobox_disk_cache_size->currentIndex());
s.setValue("delete_files", ui_->checkbox_delete_files->isChecked());
s.endGroup(); s.endGroup();
} }

View File

@ -450,6 +450,13 @@ If there are no matches then it will use the largest image in the directory.</st
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="checkbox_delete_files">
<property name="text">
<string>Enable delete files in the right click context menu</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<tabstops> <tabstops>

View File

@ -83,6 +83,8 @@ void PlaylistSettingsPage::Load() {
ui_->checkbox_editmetadatainline->setChecked(s.value("editmetadatainline", false).toBool()); ui_->checkbox_editmetadatainline->setChecked(s.value("editmetadatainline", false).toBool());
ui_->checkbox_writemetadata->setChecked(s.value(Playlist::kWriteMetadata, 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(); s.endGroup();
Init(ui_->layout_playlistsettingspage->parentWidget()); Init(ui_->layout_playlistsettingspage->parentWidget());
@ -118,6 +120,7 @@ void PlaylistSettingsPage::Save() {
s.setValue(Playlist::kPathType, static_cast<int>(path)); s.setValue(Playlist::kPathType, static_cast<int>(path));
s.setValue("editmetadatainline", ui_->checkbox_editmetadatainline->isChecked()); s.setValue("editmetadatainline", ui_->checkbox_editmetadatainline->isChecked());
s.setValue(Playlist::kWriteMetadata, ui_->checkbox_writemetadata->isChecked()); s.setValue(Playlist::kWriteMetadata, ui_->checkbox_writemetadata->isChecked());
s.setValue("delete_files", ui_->checkbox_delete_files->isChecked());
s.endGroup(); s.endGroup();
} }

View File

@ -63,6 +63,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="checkbox_delete_files">
<property name="text">
<string>Enable delete files in the right click context menu</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupbox_paths"> <widget class="QGroupBox" name="groupbox_paths">
<property name="title"> <property name="title">

View File

@ -39,6 +39,7 @@
#include "core/filesystemmusicstorage.h" #include "core/filesystemmusicstorage.h"
#include "core/iconloader.h" #include "core/iconloader.h"
#include "core/mimedata.h" #include "core/mimedata.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "fileview.h" #include "fileview.h"
#include "fileviewlist.h" #include "fileviewlist.h"
#include "ui_fileview.h" #include "ui_fileview.h"
@ -85,9 +86,7 @@ FileView::FileView(QWidget *parent)
connect(ui_->list, SIGNAL(CopyToCollection(QList<QUrl>)), SIGNAL(CopyToCollection(QList<QUrl>))); connect(ui_->list, SIGNAL(CopyToCollection(QList<QUrl>)), SIGNAL(CopyToCollection(QList<QUrl>)));
connect(ui_->list, SIGNAL(MoveToCollection(QList<QUrl>)), SIGNAL(MoveToCollection(QList<QUrl>))); connect(ui_->list, SIGNAL(MoveToCollection(QList<QUrl>)), SIGNAL(MoveToCollection(QList<QUrl>)));
connect(ui_->list, SIGNAL(CopyToDevice(QList<QUrl>)), SIGNAL(CopyToDevice(QList<QUrl>))); connect(ui_->list, SIGNAL(CopyToDevice(QList<QUrl>)), SIGNAL(CopyToDevice(QList<QUrl>)));
#ifdef HAVE_GSTREAMER
connect(ui_->list, SIGNAL(Delete(QStringList)), SLOT(Delete(QStringList))); connect(ui_->list, SIGNAL(Delete(QStringList)), SLOT(Delete(QStringList)));
#endif
connect(ui_->list, SIGNAL(EditTags(QList<QUrl>)), SIGNAL(EditTags(QList<QUrl>))); connect(ui_->list, SIGNAL(EditTags(QList<QUrl>)), SIGNAL(EditTags(QList<QUrl>)));
QString filter(FileView::kFileFilter); QString filter(FileView::kFileFilter);
@ -231,40 +230,24 @@ void FileView::UndoCommand::undo() {
void FileView::Delete(const QStringList &filenames) { void FileView::Delete(const QStringList &filenames) {
#ifdef HAVE_GSTREAMER if (filenames.isEmpty()) return;
if (filenames.isEmpty()) if (DeleteConfirmationDialog::warning(filenames) != QDialogButtonBox::Yes) return;
return;
if (QMessageBox::warning(this, tr("Delete files"), DeleteFiles *delete_files = new DeleteFiles(task_manager_, storage_, true);
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_);
connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList))); connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList)));
delete_files->Start(filenames); delete_files->Start(filenames);
#else
Q_UNUSED(filenames)
#endif
} }
void FileView::DeleteFinished(const SongList &songs_with_errors) { void FileView::DeleteFinished(const SongList &songs_with_errors) {
#ifdef HAVE_GSTREAMER
if (songs_with_errors.isEmpty()) return; if (songs_with_errors.isEmpty()) return;
OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this); OrganizeErrorDialog *dialog = new OrganizeErrorDialog(this);
dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors); dialog->Show(OrganizeErrorDialog::Type_Delete, songs_with_errors);
// It deletes itself when the user closes it // It deletes itself when the user closes it
#else
Q_UNUSED(songs_with_errors)
#endif
} }
void FileView::showEvent(QShowEvent *e) { void FileView::showEvent(QShowEvent *e) {