1 #include "library/baseplaylistfeature.h"
2 
3 #include <QFileDialog>
4 #include <QFileInfo>
5 #include <QInputDialog>
6 #include <QStandardPaths>
7 
8 #include "controllers/keyboard/keyboardeventfilter.h"
9 #include "library/export/trackexportwizard.h"
10 #include "library/library.h"
11 #include "library/parser.h"
12 #include "library/parsercsv.h"
13 #include "library/parserm3u.h"
14 #include "library/parserpls.h"
15 #include "library/playlisttablemodel.h"
16 #include "library/trackcollection.h"
17 #include "library/trackcollectionmanager.h"
18 #include "library/treeitem.h"
19 #include "moc_baseplaylistfeature.cpp"
20 #include "track/track.h"
21 #include "util/assert.h"
22 #include "widget/wlibrary.h"
23 #include "widget/wlibrarytextbrowser.h"
24 
BasePlaylistFeature(Library * pLibrary,UserSettingsPointer pConfig,PlaylistTableModel * pModel,const QString & rootViewName)25 BasePlaylistFeature::BasePlaylistFeature(
26         Library* pLibrary,
27         UserSettingsPointer pConfig,
28         PlaylistTableModel* pModel,
29         const QString& rootViewName)
30         : BaseTrackSetFeature(pLibrary, pConfig, rootViewName),
31           m_playlistDao(pLibrary->trackCollections()->internalCollection()->getPlaylistDAO()),
32           m_pPlaylistTableModel(pModel) {
33     pModel->setParent(this);
34 
35     initActions();
36 }
37 
initActions()38 void BasePlaylistFeature::initActions() {
39     m_pCreatePlaylistAction = new QAction(tr("Create New Playlist"), this);
40     connect(m_pCreatePlaylistAction,
41             &QAction::triggered,
42             this,
43             &BasePlaylistFeature::slotCreatePlaylist);
44 
45     m_pRenamePlaylistAction = new QAction(tr("Rename"), this);
46     connect(m_pRenamePlaylistAction,
47             &QAction::triggered,
48             this,
49             &BasePlaylistFeature::slotRenamePlaylist);
50     m_pDuplicatePlaylistAction = new QAction(tr("Duplicate"), this);
51     connect(m_pDuplicatePlaylistAction,
52             &QAction::triggered,
53             this,
54             &BasePlaylistFeature::slotDuplicatePlaylist);
55     m_pDeletePlaylistAction = new QAction(tr("Remove"), this);
56     connect(m_pDeletePlaylistAction,
57             &QAction::triggered,
58             this,
59             &BasePlaylistFeature::slotDeletePlaylist);
60     m_pLockPlaylistAction = new QAction(tr("Lock"), this);
61     connect(m_pLockPlaylistAction,
62             &QAction::triggered,
63             this,
64             &BasePlaylistFeature::slotTogglePlaylistLock);
65 
66     m_pAddToAutoDJAction = new QAction(tr("Add to Auto DJ Queue (bottom)"), this);
67     connect(m_pAddToAutoDJAction,
68             &QAction::triggered,
69             this,
70             &BasePlaylistFeature::slotAddToAutoDJ);
71     m_pAddToAutoDJTopAction = new QAction(tr("Add to Auto DJ Queue (top)"), this);
72     connect(m_pAddToAutoDJTopAction,
73             &QAction::triggered,
74             this,
75             &BasePlaylistFeature::slotAddToAutoDJTop);
76     m_pAddToAutoDJReplaceAction = new QAction(tr("Add to Auto DJ Queue (replace)"), this);
77     connect(m_pAddToAutoDJReplaceAction,
78             &QAction::triggered,
79             this,
80             &BasePlaylistFeature::slotAddToAutoDJReplace);
81 
82     m_pAnalyzePlaylistAction = new QAction(tr("Analyze entire Playlist"), this);
83     connect(m_pAnalyzePlaylistAction,
84             &QAction::triggered,
85             this,
86             &BasePlaylistFeature::slotAnalyzePlaylist);
87 
88     m_pImportPlaylistAction = new QAction(tr("Import Playlist"), this);
89     connect(m_pImportPlaylistAction,
90             &QAction::triggered,
91             this,
92             &BasePlaylistFeature::slotImportPlaylist);
93     m_pCreateImportPlaylistAction = new QAction(tr("Import Playlist"), this);
94     connect(m_pCreateImportPlaylistAction,
95             &QAction::triggered,
96             this,
97             &BasePlaylistFeature::slotCreateImportPlaylist);
98     m_pExportPlaylistAction = new QAction(tr("Export Playlist"), this);
99     connect(m_pExportPlaylistAction,
100             &QAction::triggered,
101             this,
102             &BasePlaylistFeature::slotExportPlaylist);
103     m_pExportTrackFilesAction = new QAction(tr("Export Track Files"), this);
104     connect(m_pExportTrackFilesAction,
105             &QAction::triggered,
106             this,
107             &BasePlaylistFeature::slotExportTrackFiles);
108 
109     connect(&m_playlistDao,
110             &PlaylistDAO::added,
111             this,
112             &BasePlaylistFeature::slotPlaylistTableChanged);
113     connect(&m_playlistDao,
114             &PlaylistDAO::lockChanged,
115             this,
116             &BasePlaylistFeature::slotPlaylistTableChanged);
117     connect(&m_playlistDao,
118             &PlaylistDAO::deleted,
119             this,
120             &BasePlaylistFeature::slotPlaylistTableChanged);
121     connect(&m_playlistDao,
122             &PlaylistDAO::tracksChanged,
123             this,
124             &BasePlaylistFeature::slotPlaylistContentChanged);
125     connect(&m_playlistDao,
126             &PlaylistDAO::renamed,
127             this,
128             &BasePlaylistFeature::slotPlaylistTableRenamed);
129 
130     connect(m_pLibrary,
131             &Library::trackSelected,
132             this,
133             &BasePlaylistFeature::slotTrackSelected);
134     connect(m_pLibrary,
135             &Library::switchToView,
136             this,
137             &BasePlaylistFeature::slotResetSelectedTrack);
138 }
139 
playlistIdFromIndex(const QModelIndex & index)140 int BasePlaylistFeature::playlistIdFromIndex(const QModelIndex& index) {
141     TreeItem* item = static_cast<TreeItem*>(index.internalPointer());
142     if (item == nullptr) {
143         return -1;
144     }
145 
146     bool ok = false;
147     int playlistId = item->getData().toInt(&ok);
148     if (ok) {
149         return playlistId;
150     } else {
151         return -1;
152     }
153 }
154 
activateChild(const QModelIndex & index)155 void BasePlaylistFeature::activateChild(const QModelIndex& index) {
156     //qDebug() << "BasePlaylistFeature::activateChild()" << index;
157     int playlistId = playlistIdFromIndex(index);
158     if (playlistId != -1) {
159         m_pPlaylistTableModel->setTableModel(playlistId);
160         emit showTrackModel(m_pPlaylistTableModel);
161         emit enableCoverArtDisplay(true);
162     }
163 }
164 
activatePlaylist(int playlistId)165 void BasePlaylistFeature::activatePlaylist(int playlistId) {
166     //qDebug() << "BasePlaylistFeature::activatePlaylist()" << playlistId;
167     QModelIndex index = indexFromPlaylistId(playlistId);
168     if (playlistId != -1 && index.isValid()) {
169         m_pPlaylistTableModel->setTableModel(playlistId);
170         emit showTrackModel(m_pPlaylistTableModel);
171         emit enableCoverArtDisplay(true);
172         // Update selection
173         emit featureSelect(this, m_lastRightClickedIndex);
174         activateChild(m_lastRightClickedIndex);
175     }
176 }
177 
slotRenamePlaylist()178 void BasePlaylistFeature::slotRenamePlaylist() {
179     int playlistId = playlistIdFromIndex(m_lastRightClickedIndex);
180     if (playlistId == -1) {
181         return;
182     }
183     QString oldName = m_playlistDao.getPlaylistName(playlistId);
184     bool locked = m_playlistDao.isPlaylistLocked(playlistId);
185 
186     if (locked) {
187         qDebug() << "Skipping playlist rename because playlist" << playlistId
188                  << "is locked.";
189         return;
190     }
191     QString newName;
192     bool validNameGiven = false;
193 
194     while (!validNameGiven) {
195         bool ok = false;
196         newName = QInputDialog::getText(nullptr,
197                 tr("Rename Playlist"),
198                 tr("Enter new name for playlist:"),
199                 QLineEdit::Normal,
200                 oldName,
201                 &ok)
202                           .trimmed();
203         if (!ok || oldName == newName) {
204             return;
205         }
206 
207         int existingId = m_playlistDao.getPlaylistIdFromName(newName);
208 
209         if (existingId != -1) {
210             QMessageBox::warning(nullptr,
211                     tr("Renaming Playlist Failed"),
212                     tr("A playlist by that name already exists."));
213         } else if (newName.isEmpty()) {
214             QMessageBox::warning(nullptr,
215                     tr("Renaming Playlist Failed"),
216                     tr("A playlist cannot have a blank name."));
217         } else {
218             validNameGiven = true;
219         }
220     }
221 
222     m_playlistDao.renamePlaylist(playlistId, newName);
223 }
224 
slotDuplicatePlaylist()225 void BasePlaylistFeature::slotDuplicatePlaylist() {
226     int oldPlaylistId = playlistIdFromIndex(m_lastRightClickedIndex);
227     if (oldPlaylistId == -1) {
228         return;
229     }
230 
231     QString oldName = m_playlistDao.getPlaylistName(oldPlaylistId);
232 
233     QString name;
234     bool validNameGiven = false;
235 
236     while (!validNameGiven) {
237         bool ok = false;
238         name = QInputDialog::getText(nullptr,
239                 tr("Duplicate Playlist"),
240                 tr("Enter name for new playlist:"),
241                 QLineEdit::Normal,
242                 //: Appendix to default name when duplicating a playlist
243                 oldName + tr("_copy", "[noun]"),
244                 &ok)
245                        .trimmed();
246         if (!ok || oldName == name) {
247             return;
248         }
249 
250         int existingId = m_playlistDao.getPlaylistIdFromName(name);
251 
252         if (existingId != -1) {
253             QMessageBox::warning(nullptr,
254                     tr("Playlist Creation Failed"),
255                     tr("A playlist by that name already exists."));
256         } else if (name.isEmpty()) {
257             QMessageBox::warning(nullptr,
258                     tr("Playlist Creation Failed"),
259                     tr("A playlist cannot have a blank name."));
260         } else {
261             validNameGiven = true;
262         }
263     }
264 
265     int newPlaylistId = m_playlistDao.createPlaylist(name);
266 
267     if (newPlaylistId != -1 &&
268             m_playlistDao.copyPlaylistTracks(oldPlaylistId, newPlaylistId)) {
269         activatePlaylist(newPlaylistId);
270     }
271 }
272 
slotTogglePlaylistLock()273 void BasePlaylistFeature::slotTogglePlaylistLock() {
274     int playlistId = playlistIdFromIndex(m_lastRightClickedIndex);
275     if (playlistId == -1) {
276         return;
277     }
278     bool locked = !m_playlistDao.isPlaylistLocked(playlistId);
279 
280     if (!m_playlistDao.setPlaylistLocked(playlistId, locked)) {
281         qDebug() << "Failed to toggle lock of playlistId " << playlistId;
282     }
283 }
284 
slotCreatePlaylist()285 void BasePlaylistFeature::slotCreatePlaylist() {
286     QString name;
287     bool validNameGiven = false;
288 
289     while (!validNameGiven) {
290         bool ok = false;
291         name = QInputDialog::getText(nullptr,
292                 tr("Create New Playlist"),
293                 tr("Enter name for new playlist:"),
294                 QLineEdit::Normal,
295                 tr("New Playlist"),
296                 &ok)
297                        .trimmed();
298         if (!ok) {
299             return;
300         }
301 
302         int existingId = m_playlistDao.getPlaylistIdFromName(name);
303 
304         if (existingId != -1) {
305             QMessageBox::warning(nullptr,
306                     tr("Playlist Creation Failed"),
307                     tr("A playlist by that name already exists."));
308         } else if (name.isEmpty()) {
309             QMessageBox::warning(nullptr,
310                     tr("Playlist Creation Failed"),
311                     tr("A playlist cannot have a blank name."));
312         } else {
313             validNameGiven = true;
314         }
315     }
316 
317     int playlistId = m_playlistDao.createPlaylist(name);
318 
319     if (playlistId != -1) {
320         activatePlaylist(playlistId);
321     } else {
322         QMessageBox::warning(nullptr,
323                 tr("Playlist Creation Failed"),
324                 tr("An unknown error occurred while creating playlist: ") + name);
325     }
326 }
327 
slotDeletePlaylist()328 void BasePlaylistFeature::slotDeletePlaylist() {
329     //qDebug() << "slotDeletePlaylist() row:" << m_lastRightClickedIndex.data();
330     int playlistId = playlistIdFromIndex(m_lastRightClickedIndex);
331     if (playlistId == -1) {
332         return;
333     }
334 
335     bool locked = m_playlistDao.isPlaylistLocked(playlistId);
336     if (locked) {
337         qDebug() << "Skipping playlist deletion because playlist" << playlistId << "is locked.";
338         return;
339     }
340 
341     if (m_lastRightClickedIndex.isValid()) {
342         VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) {
343             return;
344         }
345 
346         m_playlistDao.deletePlaylist(playlistId);
347         activate();
348     }
349 }
350 
slotImportPlaylist()351 void BasePlaylistFeature::slotImportPlaylist() {
352     //qDebug() << "slotImportPlaylist() row:" << m_lastRightClickedIndex.data();
353     QString playlist_file = getPlaylistFile();
354     if (playlist_file.isEmpty()) {
355         return;
356     }
357 
358     // Update the import/export playlist directory
359     QFileInfo fileName(playlist_file);
360     m_pConfig->set(ConfigKey("[Library]", "LastImportExportPlaylistDirectory"),
361             ConfigValue(fileName.dir().absolutePath()));
362 
363     slotImportPlaylistFile(playlist_file);
364     activateChild(m_lastRightClickedIndex);
365 }
366 
slotImportPlaylistFile(const QString & playlist_file)367 void BasePlaylistFeature::slotImportPlaylistFile(const QString& playlist_file) {
368     // The user has picked a new directory via a file dialog. This means the
369     // system sandboxer (if we are sandboxed) has granted us permission to this
370     // folder. We don't need access to this file on a regular basis so we do not
371     // register a security bookmark.
372 
373     Parser* playlist_parser = nullptr;
374 
375     if (playlist_file.endsWith(".m3u", Qt::CaseInsensitive) ||
376             playlist_file.endsWith(".m3u8", Qt::CaseInsensitive)) {
377         playlist_parser = new ParserM3u();
378     } else if (playlist_file.endsWith(".pls", Qt::CaseInsensitive)) {
379         playlist_parser = new ParserPls();
380     } else if (playlist_file.endsWith(".csv", Qt::CaseInsensitive)) {
381         playlist_parser = new ParserCsv();
382     } else {
383         return;
384     }
385 
386     if (playlist_parser) {
387         QStringList entries = playlist_parser->parse(playlist_file);
388 
389         // Iterate over the List that holds URLs of playlist entries
390         m_pPlaylistTableModel->addTracks(QModelIndex(), entries);
391 
392         // delete the parser object
393         delete playlist_parser;
394     }
395 }
396 
slotCreateImportPlaylist()397 void BasePlaylistFeature::slotCreateImportPlaylist() {
398     // Get file to read
399     QStringList playlist_files = LibraryFeature::getPlaylistFiles();
400     if (playlist_files.isEmpty()) {
401         return;
402     }
403 
404     // Set last import directory
405     QFileInfo fileName(playlist_files.first());
406     m_pConfig->set(ConfigKey("[Library]", "LastImportExportPlaylistDirectory"),
407             ConfigValue(fileName.dir().absolutePath()));
408 
409     int lastPlaylistId = -1;
410 
411     // For each selected element create a different playlist.
412     for (const QString& playlistFile : playlist_files) {
413         fileName = QFileInfo(playlistFile);
414 
415         // Get a valid name
416         QString baseName = fileName.baseName();
417         QString name;
418 
419         bool validNameGiven = false;
420         int i = 0;
421         while (!validNameGiven) {
422             name = baseName;
423             if (i != 0) {
424                 name += QString::number(i);
425             }
426 
427             // Check name
428             int existingId = m_playlistDao.getPlaylistIdFromName(name);
429 
430             validNameGiven = (existingId == -1);
431             ++i;
432         }
433 
434         lastPlaylistId = m_playlistDao.createPlaylist(name);
435         if (lastPlaylistId != -1) {
436             m_pPlaylistTableModel->setTableModel(lastPlaylistId);
437         } else {
438             QMessageBox::warning(nullptr,
439                     tr("Playlist Creation Failed"),
440                     tr("An unknown error occurred while creating playlist: ") + name);
441             return;
442         }
443 
444         slotImportPlaylistFile(playlistFile);
445     }
446     activatePlaylist(lastPlaylistId);
447 }
448 
slotExportPlaylist()449 void BasePlaylistFeature::slotExportPlaylist() {
450     int playlistId = playlistIdFromIndex(m_lastRightClickedIndex);
451     if (playlistId == -1) {
452         return;
453     }
454     QString playlistName = m_playlistDao.getPlaylistName(playlistId);
455     qDebug() << "Export playlist" << playlistName;
456 
457     QString lastPlaylistDirectory = m_pConfig->getValue(
458             ConfigKey("[Library]", "LastImportExportPlaylistDirectory"),
459             QStandardPaths::writableLocation(QStandardPaths::MusicLocation));
460 
461     // Open a dialog to let the user choose the file location for playlist export.
462     // The location is set to the last used directory for import/export and the file
463     // name to the playlist name.
464     QString filefilter = tr("M3U Playlist (*.m3u)");
465     QString file_location = QFileDialog::getSaveFileName(
466             nullptr,
467             tr("Export Playlist"),
468             lastPlaylistDirectory.append("/").append(playlistName),
469             tr("M3U Playlist (*.m3u);;M3U8 Playlist (*.m3u8);;"
470                "PLS Playlist (*.pls);;Text CSV (*.csv);;Readable Text (*.txt)"),
471             &filefilter);
472     // Exit method if user cancelled the open dialog.
473     if (file_location.isNull() || file_location.isEmpty()) {
474         return;
475     }
476     QFileInfo fileName(file_location);
477     // Update the import/export playlist directory
478     m_pConfig->set(ConfigKey("[Library]", "LastImportExportPlaylistDirectory"),
479             ConfigValue(fileName.dir().absolutePath()));
480 
481     // The user has picked a new directory via a file dialog. This means the
482     // system sandboxer (if we are sandboxed) has granted us permission to this
483     // folder. We don't need access to this file on a regular basis so we do not
484     // register a security bookmark.
485 
486     // Create a new table model since the main one might have an active search.
487     // This will only export songs that we think exist on default
488     QScopedPointer<PlaylistTableModel> pPlaylistTableModel(
489             new PlaylistTableModel(this, m_pLibrary->trackCollections(), "mixxx.db.model.playlist_export"));
490 
491     pPlaylistTableModel->setTableModel(m_pPlaylistTableModel->getPlaylist());
492     pPlaylistTableModel->setSort(pPlaylistTableModel->fieldIndex(
493                                          ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION),
494             Qt::AscendingOrder);
495     pPlaylistTableModel->select();
496 
497     // check config if relative paths are desired
498     bool useRelativePath = m_pConfig->getValue<bool>(
499             ConfigKey("[Library]", "UseRelativePathOnExport"));
500 
501     if (file_location.endsWith(".csv", Qt::CaseInsensitive)) {
502         ParserCsv::writeCSVFile(file_location, pPlaylistTableModel.data(), useRelativePath);
503     } else if (file_location.endsWith(".txt", Qt::CaseInsensitive)) {
504         if (m_playlistDao.getHiddenType(pPlaylistTableModel->getPlaylist()) == PlaylistDAO::PLHT_SET_LOG) {
505             ParserCsv::writeReadableTextFile(file_location, pPlaylistTableModel.data(), true);
506         } else {
507             ParserCsv::writeReadableTextFile(file_location, pPlaylistTableModel.data(), false);
508         }
509     } else {
510         // Create and populate a list of files of the playlist
511         QList<QString> playlist_items;
512         int rows = pPlaylistTableModel->rowCount();
513         for (int i = 0; i < rows; ++i) {
514             QModelIndex index = pPlaylistTableModel->index(i, 0);
515             playlist_items << pPlaylistTableModel->getTrackLocation(index);
516         }
517         exportPlaylistItemsIntoFile(
518                 file_location,
519                 playlist_items,
520                 useRelativePath);
521     }
522 }
523 
slotExportTrackFiles()524 void BasePlaylistFeature::slotExportTrackFiles() {
525     QScopedPointer<PlaylistTableModel> pPlaylistTableModel(
526             new PlaylistTableModel(this, m_pLibrary->trackCollections(), "mixxx.db.model.playlist_export"));
527 
528     pPlaylistTableModel->setTableModel(m_pPlaylistTableModel->getPlaylist());
529     pPlaylistTableModel->setSort(pPlaylistTableModel->fieldIndex(
530                                          ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION),
531             Qt::AscendingOrder);
532     pPlaylistTableModel->select();
533 
534     int rows = pPlaylistTableModel->rowCount();
535     TrackPointerList tracks;
536     for (int i = 0; i < rows; ++i) {
537         QModelIndex index = pPlaylistTableModel->index(i, 0);
538         tracks.push_back(pPlaylistTableModel->getTrack(index));
539     }
540 
541     TrackExportWizard track_export(nullptr, m_pConfig, tracks);
542     track_export.exportTracks();
543 }
544 
slotAddToAutoDJ()545 void BasePlaylistFeature::slotAddToAutoDJ() {
546     //qDebug() << "slotAddToAutoDJ() row:" << m_lastRightClickedIndex.data();
547     addToAutoDJ(PlaylistDAO::AutoDJSendLoc::BOTTOM);
548 }
549 
slotAddToAutoDJTop()550 void BasePlaylistFeature::slotAddToAutoDJTop() {
551     //qDebug() << "slotAddToAutoDJTop() row:" << m_lastRightClickedIndex.data();
552     addToAutoDJ(PlaylistDAO::AutoDJSendLoc::TOP);
553 }
554 
slotAddToAutoDJReplace()555 void BasePlaylistFeature::slotAddToAutoDJReplace() {
556     //qDebug() << "slotAddToAutoDJReplace() row:" << m_lastRightClickedIndex.data();
557     addToAutoDJ(PlaylistDAO::AutoDJSendLoc::REPLACE);
558 }
559 
addToAutoDJ(PlaylistDAO::AutoDJSendLoc loc)560 void BasePlaylistFeature::addToAutoDJ(PlaylistDAO::AutoDJSendLoc loc) {
561     //qDebug() << "slotAddToAutoDJ() row:" << m_lastRightClickedIndex.data();
562     if (m_lastRightClickedIndex.isValid()) {
563         int playlistId = playlistIdFromIndex(m_lastRightClickedIndex);
564         if (playlistId >= 0) {
565             // Insert this playlist
566             m_playlistDao.addPlaylistToAutoDJQueue(playlistId, loc);
567         }
568     }
569 }
570 
slotAnalyzePlaylist()571 void BasePlaylistFeature::slotAnalyzePlaylist() {
572     if (m_lastRightClickedIndex.isValid()) {
573         int playlistId = playlistIdFromIndex(m_lastRightClickedIndex);
574         if (playlistId >= 0) {
575             QList<TrackId> ids = m_playlistDao.getTrackIds(playlistId);
576             emit analyzeTracks(ids);
577         }
578     }
579 }
580 
getChildModel()581 TreeItemModel* BasePlaylistFeature::getChildModel() {
582     return &m_childModel;
583 }
584 
bindLibraryWidget(WLibrary * libraryWidget,KeyboardEventFilter * keyboard)585 void BasePlaylistFeature::bindLibraryWidget(WLibrary* libraryWidget,
586         KeyboardEventFilter* keyboard) {
587     Q_UNUSED(keyboard);
588     WLibraryTextBrowser* edit = new WLibraryTextBrowser(libraryWidget);
589     edit->setHtml(getRootViewHtml());
590     edit->setOpenLinks(false);
591     connect(edit,
592             &WLibraryTextBrowser::anchorClicked,
593             this,
594             &BasePlaylistFeature::htmlLinkClicked);
595     libraryWidget->registerView(m_rootViewName, edit);
596 }
597 
htmlLinkClicked(const QUrl & link)598 void BasePlaylistFeature::htmlLinkClicked(const QUrl& link) {
599     if (QString(link.path()) == "create") {
600         slotCreatePlaylist();
601     } else {
602         qDebug() << "Unknown playlist link clicked" << link.path();
603     }
604 }
605 
606 /**
607   * Purpose: When inserting or removing playlists,
608   * we require the sidebar model not to reset.
609   * This method queries the database and does dynamic insertion
610 */
constructChildModel(int selected_id)611 QModelIndex BasePlaylistFeature::constructChildModel(int selected_id) {
612     QList<TreeItem*> data_list;
613     int selected_row = -1;
614 
615     int row = 0;
616     const QList<IdAndLabel> playlistLabels = createPlaylistLabels();
617     for (const auto& idAndLabel : playlistLabels) {
618         int playlistId = idAndLabel.id;
619         QString playlistLabel = idAndLabel.label;
620 
621         if (selected_id == playlistId) {
622             // save index for selection
623             selected_row = row;
624         }
625 
626         // Create the TreeItem whose parent is the invisible root item
627         TreeItem* item = new TreeItem(playlistLabel, playlistId);
628         item->setBold(m_playlistsSelectedTrackIsIn.contains(playlistId));
629 
630         decorateChild(item, playlistId);
631         data_list.append(item);
632 
633         ++row;
634     }
635 
636     // Append all the newly created TreeItems in a dynamic way to the childmodel
637     m_childModel.insertTreeItemRows(data_list, 0);
638     if (selected_row == -1) {
639         return QModelIndex();
640     }
641     return m_childModel.index(selected_row, 0);
642 }
643 
updateChildModel(int playlistId)644 void BasePlaylistFeature::updateChildModel(int playlistId) {
645     QString playlistLabel = fetchPlaylistLabel(playlistId);
646 
647     QVariant variantId = QVariant(playlistId);
648 
649     for (int row = 0; row < m_childModel.rowCount(); ++row) {
650         QModelIndex index = m_childModel.index(row, 0);
651         TreeItem* pTreeItem = m_childModel.getItem(index);
652         DEBUG_ASSERT(pTreeItem != nullptr);
653         if (!pTreeItem->hasChildren() && // leaf node
654                 pTreeItem->getData() == variantId) {
655             pTreeItem->setLabel(playlistLabel);
656             decorateChild(pTreeItem, playlistId);
657         }
658     }
659 }
660 
661 /**
662   * Clears the child model dynamically, but the invisible root item remains
663   */
clearChildModel()664 void BasePlaylistFeature::clearChildModel() {
665     m_childModel.removeRows(0, m_childModel.rowCount());
666 }
667 
indexFromPlaylistId(int playlistId)668 QModelIndex BasePlaylistFeature::indexFromPlaylistId(int playlistId) {
669     QVariant variantId = QVariant(playlistId);
670     for (int row = 0; row < m_childModel.rowCount(); ++row) {
671         QModelIndex index = m_childModel.index(row, 0);
672         TreeItem* pTreeItem = m_childModel.getItem(index);
673         DEBUG_ASSERT(pTreeItem != nullptr);
674         if (!pTreeItem->hasChildren() && // leaf node
675                 pTreeItem->getData() == variantId) {
676             return index;
677         }
678     }
679     return QModelIndex();
680 }
681 
slotTrackSelected(TrackPointer pTrack)682 void BasePlaylistFeature::slotTrackSelected(TrackPointer pTrack) {
683     m_pSelectedTrack = pTrack;
684     TrackId trackId;
685     if (pTrack) {
686         trackId = pTrack->getId();
687     }
688     m_playlistDao.getPlaylistsTrackIsIn(trackId, &m_playlistsSelectedTrackIsIn);
689 
690     // Set all playlists the track is in bold (or if there is no track selected,
691     // clear all the bolding).
692     for (int row = 0; row < m_childModel.rowCount(); ++row) {
693         QModelIndex index = m_childModel.index(row, 0);
694         TreeItem* pTreeItem = m_childModel.getItem(index);
695         DEBUG_ASSERT(pTreeItem != nullptr);
696         if (!pTreeItem->hasChildren()) { // leaf node
697             bool ok;
698             int playlistId = pTreeItem->getData().toInt(&ok);
699             VERIFY_OR_DEBUG_ASSERT(ok) {
700                 continue;
701             }
702             bool shouldBold = m_playlistsSelectedTrackIsIn.contains(playlistId);
703             pTreeItem->setBold(shouldBold);
704         }
705     }
706 
707     m_childModel.triggerRepaint();
708 }
709 
slotResetSelectedTrack()710 void BasePlaylistFeature::slotResetSelectedTrack() {
711     slotTrackSelected(TrackPointer());
712 }
713