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