1 #include "library/crate/cratefeature.h"
2 
3 #include <QDesktopServices>
4 #include <QFileDialog>
5 #include <QInputDialog>
6 #include <QLineEdit>
7 #include <QMenu>
8 #include <algorithm>
9 #include <vector>
10 
11 #include "library/crate/cratefeaturehelper.h"
12 #include "library/export/trackexportwizard.h"
13 #include "library/library.h"
14 #include "library/parser.h"
15 #include "library/parsercsv.h"
16 #include "library/parserm3u.h"
17 #include "library/parserpls.h"
18 #include "library/trackcollection.h"
19 #include "library/trackcollectionmanager.h"
20 #include "library/treeitem.h"
21 #include "moc_cratefeature.cpp"
22 #include "sources/soundsourceproxy.h"
23 #include "track/track.h"
24 #include "util/dnd.h"
25 #include "widget/wlibrary.h"
26 #include "widget/wlibrarysidebar.h"
27 #include "widget/wlibrarytextbrowser.h"
28 
29 namespace {
30 
formatLabel(const CrateSummary & crateSummary)31 QString formatLabel(
32         const CrateSummary& crateSummary) {
33     return QString("%1 (%2) %3").arg(
34         crateSummary.getName(),
35         QString::number(crateSummary.getTrackCount()),
36         crateSummary.getTrackDurationText());
37 }
38 
39 } // anonymous namespace
40 
CrateFeature(Library * pLibrary,UserSettingsPointer pConfig)41 CrateFeature::CrateFeature(Library* pLibrary,
42                            UserSettingsPointer pConfig)
43         : BaseTrackSetFeature(pLibrary, pConfig, "CRATEHOME"),
44           m_cratesIcon(":/images/library/ic_library_crates.svg"),
45           m_lockedCrateIcon(":/images/library/ic_library_locked_tracklist.svg"),
46           m_pTrackCollection(pLibrary->trackCollections()->internalCollection()),
47           m_crateTableModel(this, pLibrary->trackCollections()) {
48 
49     initActions();
50 
51     // construct child model
52     m_childModel.setRootItem(TreeItem::newRoot(this));
53     rebuildChildModel();
54 
55     connectLibrary(pLibrary);
56     connectTrackCollection();
57 }
58 
initActions()59 void CrateFeature::initActions() {
60     m_pCreateCrateAction = make_parented<QAction>(tr("Create New Crate"), this);
61     connect(m_pCreateCrateAction.get(),
62             &QAction::triggered,
63             this,
64             &CrateFeature::slotCreateCrate);
65 
66     m_pRenameCrateAction = make_parented<QAction>(tr("Rename"), this);
67     connect(m_pRenameCrateAction.get(),
68             &QAction::triggered,
69             this,
70             &CrateFeature::slotRenameCrate);
71     m_pDuplicateCrateAction = make_parented<QAction>(tr("Duplicate"), this);
72     connect(m_pDuplicateCrateAction.get(),
73             &QAction::triggered,
74             this,
75             &CrateFeature::slotDuplicateCrate);
76     m_pDeleteCrateAction = make_parented<QAction>(tr("Remove"), this);
77     connect(m_pDeleteCrateAction.get(),
78             &QAction::triggered,
79             this,
80             &CrateFeature::slotDeleteCrate);
81     m_pLockCrateAction = make_parented<QAction>(tr("Lock"), this);
82     connect(m_pLockCrateAction.get(),
83             &QAction::triggered,
84             this,
85             &CrateFeature::slotToggleCrateLock);
86 
87     m_pAutoDjTrackSourceAction = make_parented<QAction>(tr("Auto DJ Track Source"), this);
88     m_pAutoDjTrackSourceAction->setCheckable(true);
89     connect(m_pAutoDjTrackSourceAction.get(),
90             &QAction::changed,
91             this,
92             &CrateFeature::slotAutoDjTrackSourceChanged);
93 
94     m_pAnalyzeCrateAction = make_parented<QAction>(tr("Analyze entire Crate"), this);
95     connect(m_pAnalyzeCrateAction.get(),
96             &QAction::triggered,
97             this,
98             &CrateFeature::slotAnalyzeCrate);
99 
100     m_pImportPlaylistAction = make_parented<QAction>(tr("Import Crate"), this);
101     connect(m_pImportPlaylistAction.get(),
102             &QAction::triggered,
103             this,
104             &CrateFeature::slotImportPlaylist);
105     m_pCreateImportPlaylistAction = make_parented<QAction>(tr("Import Crate"), this);
106     connect(m_pCreateImportPlaylistAction.get(),
107             &QAction::triggered,
108             this,
109             &CrateFeature::slotCreateImportCrate);
110     m_pExportPlaylistAction = make_parented<QAction>(tr("Export Crate"), this);
111     connect(m_pExportPlaylistAction.get(),
112             &QAction::triggered,
113             this,
114             &CrateFeature::slotExportPlaylist);
115     m_pExportTrackFilesAction = make_parented<QAction>(tr("Export Track Files"), this);
116     connect(m_pExportTrackFilesAction.get(),
117             &QAction::triggered,
118             this,
119             &CrateFeature::slotExportTrackFiles);
120 }
121 
connectLibrary(Library * pLibrary)122 void CrateFeature::connectLibrary(Library* pLibrary) {
123     connect(pLibrary,
124             &Library::trackSelected,
125             this,
126             &CrateFeature::slotTrackSelected);
127     connect(pLibrary,
128             &Library::switchToView,
129             this,
130             &CrateFeature::slotResetSelectedTrack);
131 }
132 
connectTrackCollection()133 void CrateFeature::connectTrackCollection() {
134     connect(m_pTrackCollection,
135             &TrackCollection::crateInserted,
136             this,
137             &CrateFeature::slotCrateTableChanged);
138     connect(m_pTrackCollection,
139             &TrackCollection::crateUpdated,
140             this,
141             &CrateFeature::slotCrateTableChanged);
142     connect(m_pTrackCollection,
143             &TrackCollection::crateDeleted,
144             this,
145             &CrateFeature::slotCrateTableChanged);
146     connect(m_pTrackCollection,
147             &TrackCollection::crateTracksChanged,
148             this,
149             &CrateFeature::slotCrateContentChanged);
150     connect(m_pTrackCollection,
151             &TrackCollection::crateSummaryChanged,
152             this,
153             &CrateFeature::slotUpdateCrateLabels);
154 }
155 
title()156 QVariant CrateFeature::title() {
157     return tr("Crates");
158 }
159 
getIcon()160 QIcon CrateFeature::getIcon() {
161     return m_cratesIcon;
162 }
163 
formatRootViewHtml() const164 QString CrateFeature::formatRootViewHtml() const {
165     QString cratesTitle = tr("Crates");
166     QString cratesSummary = tr("Crates are a great way to help organize the music you want to DJ with.");
167     QString cratesSummary2 = tr("Make a crate for your next gig, for your favorite electrohouse tracks, or for your most requested songs.");
168     QString cratesSummary3 = tr("Crates let you organize your music however you'd like!");
169 
170     QString html;
171     QString createCrateLink = tr("Create New Crate");
172     html.append(QString("<h2>%1</h2>").arg(cratesTitle));
173     html.append(QString("<p>%1</p>").arg(cratesSummary));
174     html.append(QString("<p>%1</p>").arg(cratesSummary2));
175     html.append(QString("<p>%1</p>").arg(cratesSummary3));
176     //Colorize links in lighter blue, instead of QT default dark blue.
177     //Links are still different from regular text, but readable on dark/light backgrounds.
178     //https://bugs.launchpad.net/mixxx/+bug/1744816
179     html.append(QString("<a style=\"color:#0496FF;\" href=\"create\">%1</a>")
180                 .arg(createCrateLink));
181     return html;
182 }
183 
newTreeItemForCrateSummary(const CrateSummary & crateSummary)184 std::unique_ptr<TreeItem> CrateFeature::newTreeItemForCrateSummary(
185         const CrateSummary& crateSummary) {
186     auto pTreeItem = TreeItem::newRoot(this);
187     updateTreeItemForCrateSummary(pTreeItem.get(), crateSummary);
188     return pTreeItem;
189 }
190 
updateTreeItemForCrateSummary(TreeItem * pTreeItem,const CrateSummary & crateSummary) const191 void CrateFeature::updateTreeItemForCrateSummary(
192         TreeItem* pTreeItem,
193         const CrateSummary& crateSummary) const {
194     DEBUG_ASSERT(pTreeItem != nullptr);
195     if (pTreeItem->getData().isNull()) {
196         // Initialize a newly created tree item
197         pTreeItem->setData(crateSummary.getId().toVariant());
198     } else {
199         // The data (= CrateId) is immutable once it has been set
200         DEBUG_ASSERT(CrateId(pTreeItem->getData()) == crateSummary.getId());
201     }
202     // Update mutable properties
203     pTreeItem->setLabel(formatLabel(crateSummary));
204     pTreeItem->setIcon(crateSummary.isLocked() ? m_lockedCrateIcon : QIcon());
205 }
206 
207 namespace {
208 
updateTreeItemForTrackSelection(TreeItem * pTreeItem,TrackId selectedTrackId,const std::vector<CrateId> & sortedTrackCrates)209 void updateTreeItemForTrackSelection(
210         TreeItem* pTreeItem,
211         TrackId selectedTrackId,
212         const std::vector<CrateId>& sortedTrackCrates) {
213     DEBUG_ASSERT(pTreeItem != nullptr);
214     bool crateContainsSelectedTrack =
215             selectedTrackId.isValid() &&
216             std::binary_search(
217                     sortedTrackCrates.begin(),
218                     sortedTrackCrates.end(),
219                     CrateId(pTreeItem->getData()));
220     pTreeItem->setBold(crateContainsSelectedTrack);
221 }
222 
223 } // anonymous namespace
224 
dropAcceptChild(const QModelIndex & index,const QList<QUrl> & urls,QObject * pSource)225 bool CrateFeature::dropAcceptChild(
226         const QModelIndex& index, const QList<QUrl>& urls, QObject* pSource) {
227     CrateId crateId(crateIdFromIndex(index));
228     VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) {
229         return false;
230     }
231     // If a track is dropped onto a crate's name, but the track isn't in the
232     // library, then add the track to the library before adding it to the
233     // playlist.
234     // pSource != nullptr it is a drop from inside Mixxx and indicates all
235     // tracks already in the DB
236     QList<TrackId> trackIds = m_pTrackCollection->resolveTrackIdsFromUrls(urls,
237             !pSource);
238     if (!trackIds.size()) {
239         return false;
240     }
241 
242     m_pTrackCollection->addCrateTracks(crateId, trackIds);
243     return true;
244 }
245 
dragMoveAcceptChild(const QModelIndex & index,const QUrl & url)246 bool CrateFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& url) {
247     CrateId crateId(crateIdFromIndex(index));
248     if (!crateId.isValid()) {
249         return false;
250     }
251     Crate crate;
252     if (!m_pTrackCollection->crates().readCrateById(crateId, &crate) || crate.isLocked()) {
253         return false;
254     }
255     return SoundSourceProxy::isUrlSupported(url) ||
256         Parser::isPlaylistFilenameSupported(url.toLocalFile());
257 }
258 
bindLibraryWidget(WLibrary * libraryWidget,KeyboardEventFilter * keyboard)259 void CrateFeature::bindLibraryWidget(WLibrary* libraryWidget,
260                               KeyboardEventFilter* keyboard) {
261     Q_UNUSED(keyboard);
262     WLibraryTextBrowser* edit = new WLibraryTextBrowser(libraryWidget);
263     edit->setHtml(formatRootViewHtml());
264     edit->setOpenLinks(false);
265     connect(edit,
266             &WLibraryTextBrowser::anchorClicked,
267             this,
268             &CrateFeature::htmlLinkClicked);
269     libraryWidget->registerView(m_rootViewName, edit);
270 }
271 
bindSidebarWidget(WLibrarySidebar * pSidebarWidget)272 void CrateFeature::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) {
273     // store the sidebar widget pointer for later use in onRightClickChild
274     m_pSidebarWidget = pSidebarWidget;
275 }
276 
getChildModel()277 TreeItemModel* CrateFeature::getChildModel() {
278     return &m_childModel;
279 }
280 
activateChild(const QModelIndex & index)281 void CrateFeature::activateChild(const QModelIndex& index) {
282     CrateId crateId(crateIdFromIndex(index));
283     VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) {
284         return;
285     }
286     m_crateTableModel.selectCrate(crateId);
287     emit showTrackModel(&m_crateTableModel);
288     emit enableCoverArtDisplay(true);
289 }
290 
activateCrate(CrateId crateId)291 bool CrateFeature::activateCrate(CrateId crateId) {
292     qDebug() << "CrateFeature::activateCrate()" << crateId;
293     VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) {
294         return false;
295     }
296     QModelIndex index = indexFromCrateId(crateId);
297     VERIFY_OR_DEBUG_ASSERT(index.isValid()) {
298         return false;
299     }
300     m_lastRightClickedIndex = index;
301     m_crateTableModel.selectCrate(crateId);
302     emit showTrackModel(&m_crateTableModel);
303     emit enableCoverArtDisplay(true);
304     // Update selection
305     emit featureSelect(this, m_lastRightClickedIndex);
306     activateChild(m_lastRightClickedIndex);
307     return true;
308 }
309 
readLastRightClickedCrate(Crate * pCrate) const310 bool CrateFeature::readLastRightClickedCrate(Crate* pCrate) const {
311     CrateId crateId(crateIdFromIndex(m_lastRightClickedIndex));
312     VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) {
313         qWarning() << "Failed to determine id of selected crate";
314         return false;
315     }
316     VERIFY_OR_DEBUG_ASSERT(m_pTrackCollection->crates().readCrateById(crateId, pCrate)) {
317         qWarning() << "Failed to read selected crate with id" << crateId;
318         return false;
319     }
320     return true;
321 }
322 
onRightClick(const QPoint & globalPos)323 void CrateFeature::onRightClick(const QPoint& globalPos) {
324     m_lastRightClickedIndex = QModelIndex();
325     QMenu menu(m_pSidebarWidget);
326     menu.addAction(m_pCreateCrateAction.get());
327     menu.addSeparator();
328     menu.addAction(m_pCreateImportPlaylistAction.get());
329     menu.exec(globalPos);
330 }
331 
onRightClickChild(const QPoint & globalPos,const QModelIndex & index)332 void CrateFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex& index) {
333     //Save the model index so we can get it in the action slots...
334     m_lastRightClickedIndex = index;
335     CrateId crateId(crateIdFromIndex(index));
336     if (!crateId.isValid()) {
337         return;
338     }
339 
340     Crate crate;
341     if (!m_pTrackCollection->crates().readCrateById(crateId, &crate)) {
342         return;
343     }
344 
345     m_pDeleteCrateAction->setEnabled(!crate.isLocked());
346     m_pRenameCrateAction->setEnabled(!crate.isLocked());
347 
348     m_pAutoDjTrackSourceAction->setChecked(crate.isAutoDjSource());
349 
350     m_pLockCrateAction->setText(crate.isLocked() ? tr("Unlock") : tr("Lock"));
351 
352     QMenu menu(m_pSidebarWidget);
353     menu.addAction(m_pCreateCrateAction.get());
354     menu.addSeparator();
355     menu.addAction(m_pRenameCrateAction.get());
356     menu.addAction(m_pDuplicateCrateAction.get());
357     menu.addAction(m_pDeleteCrateAction.get());
358     menu.addAction(m_pLockCrateAction.get());
359     menu.addSeparator();
360     menu.addAction(m_pAutoDjTrackSourceAction.get());
361     menu.addSeparator();
362     menu.addAction(m_pAnalyzeCrateAction.get());
363     menu.addSeparator();
364     if (!crate.isLocked()) {
365         menu.addAction(m_pImportPlaylistAction.get());
366     }
367     menu.addAction(m_pExportPlaylistAction.get());
368     menu.addAction(m_pExportTrackFilesAction.get());
369     menu.exec(globalPos);
370 }
371 
slotCreateCrate()372 void CrateFeature::slotCreateCrate() {
373     CrateId crateId = CrateFeatureHelper(
374             m_pTrackCollection, m_pConfig).createEmptyCrate();
375     if (crateId.isValid()) {
376         activateCrate(crateId);
377     }
378 }
379 
slotDeleteCrate()380 void CrateFeature::slotDeleteCrate() {
381     Crate crate;
382     if (readLastRightClickedCrate(&crate)) {
383         if (crate.isLocked()) {
384             qWarning() << "Refusing to delete locked crate" << crate;
385             return;
386         }
387         if (m_pTrackCollection->deleteCrate(crate.getId())) {
388             qDebug() << "Deleted crate" << crate;
389             return;
390         }
391     }
392     qWarning() << "Failed to delete selected crate";
393 }
394 
slotRenameCrate()395 void CrateFeature::slotRenameCrate() {
396     Crate crate;
397     if (readLastRightClickedCrate(&crate)) {
398         const QString oldName = crate.getName();
399         crate.resetName();
400         for (;;) {
401             bool ok = false;
402             auto newName =
403                     QInputDialog::getText(
404                             nullptr,
405                             tr("Rename Crate"),
406                             tr("Enter new name for crate:"),
407                             QLineEdit::Normal,
408                             oldName,
409                             &ok).trimmed();
410             if (!ok || newName.isEmpty()) {
411                 return;
412             }
413             if (newName.isEmpty()) {
414                 QMessageBox::warning(
415                         nullptr,
416                         tr("Renaming Crate Failed"),
417                         tr("A crate cannot have a blank name."));
418                 continue;
419             }
420             if (m_pTrackCollection->crates().readCrateByName(newName)) {
421                 QMessageBox::warning(
422                         nullptr,
423                         tr("Renaming Crate Failed"),
424                         tr("A crate by that name already exists."));
425                 continue;
426             }
427             crate.setName(std::move(newName));
428             DEBUG_ASSERT(crate.hasName());
429             break;
430         }
431 
432         if (!m_pTrackCollection->updateCrate(crate)) {
433             qDebug() << "Failed to rename crate" << crate;
434         }
435     } else {
436         qDebug() << "Failed to rename selected crate";
437     }
438 }
439 
slotDuplicateCrate()440 void CrateFeature::slotDuplicateCrate() {
441     Crate crate;
442     if (readLastRightClickedCrate(&crate)) {
443         CrateId crateId = CrateFeatureHelper(
444                 m_pTrackCollection, m_pConfig).duplicateCrate(crate);
445         if (crateId.isValid()) {
446             activateCrate(crateId);
447         }
448     } else {
449         qDebug() << "Failed to duplicate selected crate";
450     }
451 }
452 
slotToggleCrateLock()453 void CrateFeature::slotToggleCrateLock() {
454     Crate crate;
455     if (readLastRightClickedCrate(&crate)) {
456         crate.setLocked(!crate.isLocked());
457         if (!m_pTrackCollection->updateCrate(crate)) {
458             qDebug() << "Failed to toggle lock of crate" << crate;
459         }
460     } else {
461         qDebug() << "Failed to toggle lock of selected crate";
462     }
463 }
464 
slotAutoDjTrackSourceChanged()465 void CrateFeature::slotAutoDjTrackSourceChanged() {
466     Crate crate;
467     if (readLastRightClickedCrate(&crate)) {
468         if (crate.isAutoDjSource() != m_pAutoDjTrackSourceAction->isChecked()) {
469             crate.setAutoDjSource(m_pAutoDjTrackSourceAction->isChecked());
470             m_pTrackCollection->updateCrate(crate);
471         }
472     }
473 }
474 
rebuildChildModel(CrateId selectedCrateId)475 QModelIndex CrateFeature::rebuildChildModel(CrateId selectedCrateId) {
476     qDebug() << "CrateFeature::rebuildChildModel()" << selectedCrateId;
477 
478     TreeItem* pRootItem = m_childModel.getRootItem();
479     VERIFY_OR_DEBUG_ASSERT(pRootItem != nullptr) {
480         return QModelIndex();
481     }
482     m_childModel.removeRows(0, pRootItem->childRows());
483 
484     QList<TreeItem*> modelRows;
485     modelRows.reserve(m_pTrackCollection->crates().countCrates());
486 
487     int selectedRow = -1;
488     CrateSummarySelectResult crateSummaries(
489             m_pTrackCollection->crates().selectCrateSummaries());
490     CrateSummary crateSummary;
491     while (crateSummaries.populateNext(&crateSummary)) {
492         auto pTreeItem = newTreeItemForCrateSummary(crateSummary);
493         modelRows.append(pTreeItem.get());
494         pTreeItem.release();
495         if (selectedCrateId == crateSummary.getId()) {
496             // save index for selection
497             selectedRow = modelRows.size() - 1;
498         }
499     }
500 
501     // Append all the newly created TreeItems in a dynamic way to the childmodel
502     m_childModel.insertTreeItemRows(modelRows, 0);
503 
504     // Update rendering of crates depending on the currently selected track
505     slotTrackSelected(m_pSelectedTrack);
506 
507     if (selectedRow >= 0) {
508         return m_childModel.index(selectedRow, 0);
509     } else {
510         return QModelIndex();
511     }
512 }
513 
updateChildModel(const QSet<CrateId> & updatedCrateIds)514 void CrateFeature::updateChildModel(const QSet<CrateId>& updatedCrateIds) {
515     const CrateStorage& crateStorage = m_pTrackCollection->crates();
516     for (const CrateId& crateId: updatedCrateIds) {
517         QModelIndex index = indexFromCrateId(crateId);
518         VERIFY_OR_DEBUG_ASSERT(index.isValid()) {
519             continue;
520         }
521         CrateSummary crateSummary;
522         VERIFY_OR_DEBUG_ASSERT(crateStorage.readCrateSummaryById(crateId, &crateSummary)) {
523             continue;
524         }
525         updateTreeItemForCrateSummary(m_childModel.getItem(index), crateSummary);
526         m_childModel.triggerRepaint(index);
527     }
528     if (m_pSelectedTrack) {
529         // Crates containing the currently selected track might
530         // have been modified.
531         slotTrackSelected(m_pSelectedTrack);
532     }
533 }
534 
crateIdFromIndex(const QModelIndex & index) const535 CrateId CrateFeature::crateIdFromIndex(const QModelIndex& index) const {
536     if (!index.isValid()) {
537         return CrateId();
538     }
539     TreeItem* item = static_cast<TreeItem*>(index.internalPointer());
540     if (item == nullptr) {
541         return CrateId();
542     }
543     return CrateId(item->getData());
544 }
545 
indexFromCrateId(CrateId crateId) const546 QModelIndex CrateFeature::indexFromCrateId(CrateId crateId) const {
547     VERIFY_OR_DEBUG_ASSERT(crateId.isValid()) {
548         return QModelIndex();
549     }
550     for (int row = 0; row < m_childModel.rowCount(); ++row) {
551         QModelIndex index = m_childModel.index(row, 0);
552         TreeItem* pTreeItem = m_childModel.getItem(index);
553         DEBUG_ASSERT(pTreeItem != nullptr);
554         if (!pTreeItem->hasChildren() && // leaf node
555                 (CrateId(pTreeItem->getData()) == crateId)) {
556             return index;
557         }
558     }
559     qDebug() << "Tree item for crate not found:" << crateId;
560     return QModelIndex();
561 }
562 
slotImportPlaylist()563 void CrateFeature::slotImportPlaylist() {
564     //qDebug() << "slotImportPlaylist() row:" ; //<< m_lastRightClickedIndex.data();
565 
566     QString playlist_file = getPlaylistFile();
567     if (playlist_file.isEmpty()) {
568         return;
569     }
570 
571     // Update the import/export crate directory
572     QFileInfo fileName(playlist_file);
573     m_pConfig->set(ConfigKey("[Library]","LastImportExportCrateDirectory"),
574                    ConfigValue(fileName.dir().absolutePath()));
575 
576     slotImportPlaylistFile(playlist_file);
577     activateChild(m_lastRightClickedIndex);
578 }
579 
slotImportPlaylistFile(const QString & playlist_file)580 void CrateFeature::slotImportPlaylistFile(const QString& playlist_file) {
581     // The user has picked a new directory via a file dialog. This means the
582     // system sandboxer (if we are sandboxed) has granted us permission to this
583     // folder. We don't need access to this file on a regular basis so we do not
584     // register a security bookmark.
585     // TODO(XXX): Parsing a list of track locations from a playlist file
586     // is a general task and should be implemented separately.
587     QList<QString> entries;
588     if (playlist_file.endsWith(".m3u", Qt::CaseInsensitive) ||
589         playlist_file.endsWith(".m3u8", Qt::CaseInsensitive)) {
590         // .m3u8 is Utf8 representation of an m3u playlist
591         entries = ParserM3u().parse(playlist_file);
592     } else if (playlist_file.endsWith(".pls", Qt::CaseInsensitive)) {
593         entries = ParserPls().parse(playlist_file);
594     } else if (playlist_file.endsWith(".csv", Qt::CaseInsensitive)) {
595         entries = ParserCsv().parse(playlist_file);
596     } else {
597         return;
598     }
599     m_crateTableModel.addTracks(QModelIndex(), entries);
600 }
601 
slotCreateImportCrate()602 void CrateFeature::slotCreateImportCrate() {
603 
604     // Get file to read
605     QStringList playlist_files = LibraryFeature::getPlaylistFiles();
606     if (playlist_files.isEmpty()) {
607         return;
608     }
609 
610 
611     // Set last import directory
612     QFileInfo fileName(playlist_files.first());
613     m_pConfig->set(ConfigKey("[Library]","LastImportExportCrateDirectory"),
614                 ConfigValue(fileName.dir().absolutePath()));
615 
616     CrateId lastCrateId;
617 
618     // For each selected file
619     for (const QString& playlistFile : playlist_files) {
620         fileName = QFileInfo(playlistFile);
621 
622         Crate crate;
623 
624         // Get a valid name
625         QString baseName = fileName.baseName();
626         for (int i = 0;; ++i) {
627             auto name = baseName;
628             if (i > 0) {
629                 name += QString(" %1").arg(i);
630             }
631             name = name.trimmed();
632             if (!name.isEmpty()) {
633                 if (!m_pTrackCollection->crates().readCrateByName(name)) {
634                     // unused crate name found
635                     crate.setName(std::move(name));
636                     DEBUG_ASSERT(crate.hasName());
637                     break; // terminate loop
638                 }
639             }
640         }
641 
642         if (m_pTrackCollection->insertCrate(crate, &lastCrateId)) {
643             m_crateTableModel.selectCrate(lastCrateId);
644         } else {
645             QMessageBox::warning(
646                     nullptr,
647                     tr("Crate Creation Failed"),
648                     tr("An unknown error occurred while creating crate: ") + crate.getName());
649             return;
650         }
651 
652         slotImportPlaylistFile(playlistFile);
653     }
654     activateCrate(lastCrateId);
655 }
656 
slotAnalyzeCrate()657 void CrateFeature::slotAnalyzeCrate() {
658     if (m_lastRightClickedIndex.isValid()) {
659         CrateId crateId = crateIdFromIndex(m_lastRightClickedIndex);
660         if (crateId.isValid()) {
661             QList<TrackId> trackIds;
662             trackIds.reserve(
663                     m_pTrackCollection->crates().countCrateTracks(crateId));
664             {
665                 CrateTrackSelectResult crateTracks(
666                         m_pTrackCollection->crates().selectCrateTracksSorted(crateId));
667                 while (crateTracks.next()) {
668                     trackIds.append(crateTracks.trackId());
669                 }
670             }
671             emit analyzeTracks(trackIds);
672         }
673     }
674 }
675 
slotExportPlaylist()676 void CrateFeature::slotExportPlaylist() {
677     CrateId crateId = m_crateTableModel.selectedCrate();
678     Crate crate;
679     if (m_pTrackCollection->crates().readCrateById(crateId, &crate)) {
680         qDebug() << "Exporting crate" << crate;
681     } else {
682         qDebug() << "Failed to export crate" << crateId;
683     }
684 
685     QString lastCrateDirectory = m_pConfig->getValue(
686             ConfigKey("[Library]", "LastImportExportCrateDirectory"),
687             QStandardPaths::writableLocation(QStandardPaths::MusicLocation));
688 
689     QString file_location = QFileDialog::getSaveFileName(nullptr,
690             tr("Export Crate"),
691             lastCrateDirectory.append("/").append(crate.getName()),
692             tr("M3U Playlist (*.m3u);;M3U8 Playlist (*.m3u8);;PLS Playlist "
693                "(*.pls);;Text CSV (*.csv);;Readable Text (*.txt)"));
694     // Exit method if user cancelled the open dialog.
695     if (file_location.isNull() || file_location.isEmpty()) {
696         return;
697     }
698 
699     // Update the import/export crate directory
700     QFileInfo fileName(file_location);
701     m_pConfig->set(ConfigKey("[Library]","LastImportExportCrateDirectory"),
702                 ConfigValue(fileName.dir().absolutePath()));
703 
704     // The user has picked a new directory via a file dialog. This means the
705     // system sandboxer (if we are sandboxed) has granted us permission to this
706     // folder. We don't need access to this file on a regular basis so we do not
707     // register a security bookmark.
708 
709     // check config if relative paths are desired
710     bool useRelativePath =
711         m_pConfig->getValue<bool>(
712             ConfigKey("[Library]", "UseRelativePathOnExport"));
713 
714     // Create list of files of the crate
715     // Create a new table model since the main one might have an active search.
716     QScopedPointer<CrateTableModel> pCrateTableModel(
717         new CrateTableModel(this, m_pLibrary->trackCollections()));
718     pCrateTableModel->selectCrate(m_crateTableModel.selectedCrate());
719     pCrateTableModel->select();
720 
721     if (file_location.endsWith(".csv", Qt::CaseInsensitive)) {
722         ParserCsv::writeCSVFile(file_location, pCrateTableModel.data(), useRelativePath);
723     } else if (file_location.endsWith(".txt", Qt::CaseInsensitive)) {
724         ParserCsv::writeReadableTextFile(file_location, pCrateTableModel.data(), false);
725     } else{
726         // populate a list of files of the crate
727         QList<QString> playlist_items;
728         int rows = pCrateTableModel->rowCount();
729         for (int i = 0; i < rows; ++i) {
730             QModelIndex index = m_crateTableModel.index(i, 0);
731             playlist_items << m_crateTableModel.getTrackLocation(index);
732         }
733         exportPlaylistItemsIntoFile(
734                 file_location,
735                 playlist_items,
736                 useRelativePath);
737     }
738 }
739 
slotExportTrackFiles()740 void CrateFeature::slotExportTrackFiles() {
741     // Create a new table model since the main one might have an active search.
742     QScopedPointer<CrateTableModel> pCrateTableModel(
743         new CrateTableModel(this, m_pLibrary->trackCollections()));
744     pCrateTableModel->selectCrate(m_crateTableModel.selectedCrate());
745     pCrateTableModel->select();
746 
747     int rows = pCrateTableModel->rowCount();
748     TrackPointerList trackpointers;
749     for (int i = 0; i < rows; ++i) {
750         QModelIndex index = m_crateTableModel.index(i, 0);
751         trackpointers.push_back(m_crateTableModel.getTrack(index));
752     }
753 
754     TrackExportWizard track_export(nullptr, m_pConfig, trackpointers);
755     track_export.exportTracks();
756 }
757 
slotCrateTableChanged(CrateId crateId)758 void CrateFeature::slotCrateTableChanged(CrateId crateId) {
759     if (m_lastRightClickedIndex.isValid() &&
760             (crateIdFromIndex(m_lastRightClickedIndex) == crateId)) {
761         // Preserve crate selection
762         m_lastRightClickedIndex = rebuildChildModel(crateId);
763         if (m_lastRightClickedIndex.isValid()) {
764             activateCrate(crateId);
765         }
766     } else {
767         // Discard crate selection
768         rebuildChildModel();
769     }
770 }
771 
slotCrateContentChanged(CrateId crateId)772 void CrateFeature::slotCrateContentChanged(CrateId crateId) {
773     QSet<CrateId> updatedCrateIds;
774     updatedCrateIds.insert(crateId);
775     updateChildModel(updatedCrateIds);
776 }
777 
slotUpdateCrateLabels(const QSet<CrateId> & updatedCrateIds)778 void CrateFeature::slotUpdateCrateLabels(const QSet<CrateId>& updatedCrateIds) {
779     updateChildModel(updatedCrateIds);
780 }
781 
htmlLinkClicked(const QUrl & link)782 void CrateFeature::htmlLinkClicked(const QUrl& link) {
783     if (QString(link.path())=="create") {
784         slotCreateCrate();
785     } else {
786         qDebug() << "Unknown crate link clicked" << link;
787     }
788 }
789 
slotTrackSelected(TrackPointer pTrack)790 void CrateFeature::slotTrackSelected(TrackPointer pTrack) {
791     m_pSelectedTrack = std::move(pTrack);
792 
793     TreeItem* pRootItem = m_childModel.getRootItem();
794     VERIFY_OR_DEBUG_ASSERT(pRootItem != nullptr) {
795         return;
796     }
797 
798     TrackId selectedTrackId;
799     std::vector<CrateId> sortedTrackCrates;
800     if (m_pSelectedTrack) {
801         selectedTrackId = m_pSelectedTrack->getId();
802         CrateTrackSelectResult trackCratesIter(
803                 m_pTrackCollection->crates().selectTrackCratesSorted(selectedTrackId));
804         while (trackCratesIter.next()) {
805             sortedTrackCrates.push_back(trackCratesIter.crateId());
806         }
807     }
808 
809     // Set all crates the track is in bold (or if there is no track selected,
810     // clear all the bolding).
811     for (TreeItem* pTreeItem: pRootItem->children()) {
812         updateTreeItemForTrackSelection(pTreeItem, selectedTrackId, sortedTrackCrates);
813     }
814 
815     m_childModel.triggerRepaint();
816 }
817 
slotResetSelectedTrack()818 void CrateFeature::slotResetSelectedTrack() {
819     slotTrackSelected(TrackPointer());
820 }
821