diff --git a/src/mediaplaylistproxymodel.cpp b/src/mediaplaylistproxymodel.cpp index 77795e2e..49aab627 100644 --- a/src/mediaplaylistproxymodel.cpp +++ b/src/mediaplaylistproxymodel.cpp @@ -1,805 +1,806 @@ /* SPDX-FileCopyrightText: 2015 (c) Matthieu Gallien SPDX-FileCopyrightText: 2019 (c) Alexander Stippich SPDX-License-Identifier: LGPL-3.0-or-later */ #include "mediaplaylistproxymodel.h" #include "mediaplaylist.h" #include "playListLogging.h" #include #include #include #include #include #include #include #include #include class MediaPlayListProxyModelPrivate { public: MediaPlayList* mPlayListModel; QPersistentModelIndex mPreviousTrack; QPersistentModelIndex mCurrentTrack; QPersistentModelIndex mNextTrack; QMediaPlaylist mLoadPlaylist; QList mRandomMapping; QVariantMap mPersistentSettingsForUndo; QRandomGenerator mRandomGenerator; QMimeDatabase mMimeDb; ElisaUtils::PlayListEnqueueTriggerPlay mTriggerPlay = ElisaUtils::DoNotTriggerPlay; int mCurrentPlayListPosition = -1; bool mRepeatPlay = false; bool mShufflePlayList = false; }; MediaPlayListProxyModel::MediaPlayListProxyModel(QObject *parent) : QAbstractProxyModel (parent), d(std::make_unique()) { connect(&d->mLoadPlaylist, &QMediaPlaylist::loaded, this, &MediaPlayListProxyModel::loadPlayListLoaded); connect(&d->mLoadPlaylist, &QMediaPlaylist::loadFailed, this, &MediaPlayListProxyModel::loadPlayListLoadFailed); d->mRandomGenerator.seed(static_cast(QTime::currentTime().msec())); } MediaPlayListProxyModel::~MediaPlayListProxyModel() =default; QModelIndex MediaPlayListProxyModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column < 0 || row > rowCount() - 1) { return QModelIndex(); } return createIndex(row, column); Q_UNUSED(parent); } QModelIndex MediaPlayListProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) { return QModelIndex(); } return d->mPlayListModel->index(mapRowFromSource(sourceIndex.row()), sourceIndex.column()); } QItemSelection MediaPlayListProxyModel::mapSelectionFromSource(const QItemSelection &sourceSelection) const { QItemSelection proxySelection; for (const QItemSelectionRange &range : sourceSelection) { QModelIndex proxyTopLeft = mapFromSource(range.topLeft()); QModelIndex proxyBottomRight = mapFromSource(range.bottomRight()); proxySelection.append(QItemSelectionRange(proxyTopLeft, proxyBottomRight)); } return proxySelection; } QItemSelection MediaPlayListProxyModel::mapSelectionToSource(const QItemSelection &proxySelection) const { QItemSelection sourceSelection; for (const QItemSelectionRange &range : proxySelection) { QModelIndex sourceTopLeft = mapToSource(range.topLeft()); QModelIndex sourceBottomRight = mapToSource(range.bottomRight()); sourceSelection.append(QItemSelectionRange(sourceTopLeft, sourceBottomRight)); } return sourceSelection; } QModelIndex MediaPlayListProxyModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid()) { return QModelIndex(); } return d->mPlayListModel->index(mapRowToSource(proxyIndex.row()), proxyIndex.column()); } int MediaPlayListProxyModel::mapRowToSource(const int proxyRow) const { if (d->mShufflePlayList) { return d->mRandomMapping.at(proxyRow); } else { return proxyRow; } } int MediaPlayListProxyModel::mapRowFromSource(const int sourceRow) const { if (d->mShufflePlayList) { return d->mRandomMapping.indexOf(sourceRow); } else { return sourceRow; } } int MediaPlayListProxyModel::rowCount(const QModelIndex &parent) const { if (d->mShufflePlayList) { if (parent.isValid()) { return 0; } return d->mRandomMapping.count(); } else { return d->mPlayListModel->rowCount(parent); } } int MediaPlayListProxyModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QModelIndex MediaPlayListProxyModel::parent(const QModelIndex &child) const { Q_UNUSED(child); return QModelIndex(); } bool MediaPlayListProxyModel::hasChildren(const QModelIndex &parent) const { return (!parent.isValid()) ? false : (rowCount() > 0); } void MediaPlayListProxyModel::setPlayListModel(MediaPlayList *playListModel) { if (d->mPlayListModel) { disconnect(playListModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &MediaPlayListProxyModel::sourceRowsAboutToBeInserted); disconnect(playListModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeRemoved); disconnect(playListModel, &QAbstractItemModel::rowsAboutToBeMoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeMoved); disconnect(playListModel, &QAbstractItemModel::rowsInserted, this, &MediaPlayListProxyModel::sourceRowsInserted); disconnect(playListModel, &QAbstractItemModel::rowsRemoved, this, &MediaPlayListProxyModel::sourceRowsRemoved); disconnect(playListModel, &QAbstractItemModel::rowsMoved, this, &MediaPlayListProxyModel::sourceRowsMoved); disconnect(playListModel, &QAbstractItemModel::dataChanged, this, &MediaPlayListProxyModel::sourceDataChanged); disconnect(playListModel, &QAbstractItemModel::headerDataChanged, this, &MediaPlayListProxyModel::sourceHeaderDataChanged); disconnect(playListModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &MediaPlayListProxyModel::sourceLayoutAboutToBeChanged); disconnect(playListModel, &QAbstractItemModel::layoutChanged, this, &MediaPlayListProxyModel::sourceLayoutChanged); disconnect(playListModel, &QAbstractItemModel::modelAboutToBeReset, this, &MediaPlayListProxyModel::sourceModelAboutToBeReset); disconnect(playListModel, &QAbstractItemModel::modelReset, this, &MediaPlayListProxyModel::sourceModelReset); } d->mPlayListModel = playListModel; setSourceModel(playListModel); if (d->mPlayListModel) { connect(playListModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &MediaPlayListProxyModel::sourceRowsAboutToBeInserted); connect(playListModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeRemoved); connect(playListModel, &QAbstractItemModel::rowsAboutToBeMoved, this, &MediaPlayListProxyModel::sourceRowsAboutToBeMoved); connect(playListModel, &QAbstractItemModel::rowsInserted, this, &MediaPlayListProxyModel::sourceRowsInserted); connect(playListModel, &QAbstractItemModel::rowsRemoved, this, &MediaPlayListProxyModel::sourceRowsRemoved); connect(playListModel, &QAbstractItemModel::rowsMoved, this, &MediaPlayListProxyModel::sourceRowsMoved); connect(playListModel, &QAbstractItemModel::dataChanged, this, &MediaPlayListProxyModel::sourceDataChanged); connect(playListModel, &QAbstractItemModel::headerDataChanged, this, &MediaPlayListProxyModel::sourceHeaderDataChanged); connect(playListModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &MediaPlayListProxyModel::sourceLayoutAboutToBeChanged); connect(playListModel, &QAbstractItemModel::layoutChanged, this, &MediaPlayListProxyModel::sourceLayoutChanged); connect(playListModel, &QAbstractItemModel::modelAboutToBeReset, this, &MediaPlayListProxyModel::sourceModelAboutToBeReset); connect(playListModel, &QAbstractItemModel::modelReset, this, &MediaPlayListProxyModel::sourceModelReset); } } void MediaPlayListProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { QAbstractProxyModel::setSourceModel(sourceModel); } QPersistentModelIndex MediaPlayListProxyModel::previousTrack() const { return d->mPreviousTrack; } QPersistentModelIndex MediaPlayListProxyModel::currentTrack() const { return d->mCurrentTrack; } QPersistentModelIndex MediaPlayListProxyModel::nextTrack() const { return d->mNextTrack; } void MediaPlayListProxyModel::setRepeatPlay(const bool value) { if (d->mRepeatPlay != value) { d->mRepeatPlay = value; Q_EMIT repeatPlayChanged(); Q_EMIT remainingTracksChanged(); Q_EMIT persistentStateChanged(); determineAndNotifyPreviousAndNextTracks(); } } bool MediaPlayListProxyModel::repeatPlay() const { return d->mRepeatPlay; } void MediaPlayListProxyModel::setShufflePlayList(const bool value) { if (d->mShufflePlayList != value) { Q_EMIT layoutAboutToBeChanged(QList(), QAbstractItemModel::VerticalSortHint); auto playListSize = d->mPlayListModel->rowCount(); if (playListSize != 0) { if (value) { d->mRandomMapping.clear(); d->mRandomMapping.reserve(playListSize); QModelIndexList to; to.reserve(playListSize); for (int i = 0; i < playListSize; ++i) { to.append(index(i,0)); d->mRandomMapping.append(i); } QModelIndexList from; from.reserve(playListSize); // Fisher-Yates algorithm for (int i = 0; i < playListSize - 1; ++i) { const int swapIndex = d->mRandomGenerator.bounded(i, playListSize); std::swap(d->mRandomMapping[i], d->mRandomMapping[swapIndex]); from.append(index(d->mRandomMapping.at(i), 0)); } from.append(index(d->mRandomMapping.at(playListSize - 1), 0)); changePersistentIndexList(from, to); } else { QModelIndexList from; from.reserve(playListSize); QModelIndexList to; to.reserve(playListSize); for (int i = 0; i < playListSize; ++i) { to.append(index(d->mRandomMapping.at(i), 0)); from.append(index(i, 0)); } changePersistentIndexList(from, to); d->mRandomMapping.clear(); } d->mCurrentPlayListPosition = d->mCurrentTrack.row(); d->mShufflePlayList = value; Q_EMIT layoutChanged(QList(), QAbstractItemModel::VerticalSortHint); determineAndNotifyPreviousAndNextTracks(); } else { d->mShufflePlayList = value; } Q_EMIT shufflePlayListChanged(); Q_EMIT remainingTracksChanged(); Q_EMIT persistentStateChanged(); } } bool MediaPlayListProxyModel::shufflePlayList() const { return d->mShufflePlayList; } void MediaPlayListProxyModel::sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { /* * When in random mode, rows are only inserted after * the source model is done inserting new items since * new items can be added at arbitrarily positions, * which requires a split of beginInsertRows */ if (!d->mShufflePlayList) { beginInsertRows(parent, start, end); } } void MediaPlayListProxyModel::sourceRowsInserted(const QModelIndex &parent, int start, int end) { if (d->mShufflePlayList) { const auto newItemsCount = end - start + 1; d->mRandomMapping.reserve(rowCount() + newItemsCount); if (rowCount() == 0 || newItemsCount == 1) { beginInsertRows(parent, start, end); for (int i = 0; i < newItemsCount; ++i) { //QRandomGenerator.bounded(int) is exclusive, thus + 1 const auto random = d->mRandomGenerator.bounded(d->mRandomMapping.count()+1); d->mRandomMapping.insert(random, start + i); } endInsertRows(); } else { for (int i = 0; i < newItemsCount; ++i) { //QRandomGenerator.bounded(int) is exclusive, thus + 1 const auto random = d->mRandomGenerator.bounded(d->mRandomMapping.count()+1); beginInsertRows(parent, random, random); d->mRandomMapping.insert(random, start + i); endInsertRows(); } } } else { endInsertRows(); } determineTracks(); Q_EMIT tracksCountChanged(); Q_EMIT remainingTracksChanged(); Q_EMIT persistentStateChanged(); } void MediaPlayListProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { if (d->mShufflePlayList) { if (end - start + 1 == rowCount()) { beginRemoveRows(parent, start, end); d->mRandomMapping.clear(); endRemoveRows(); } int row = 0; auto it = d->mRandomMapping.begin(); while (it != d->mRandomMapping.end()) { if (*it >= start && *it <= end){ beginRemoveRows(parent, row, row); it = d->mRandomMapping.erase(it); endRemoveRows(); } else { if (*it > end) { *it = *it - end + start - 1; } it++; row++; } } } else { beginRemoveRows(parent, start, end); } } void MediaPlayListProxyModel::sourceRowsRemoved(const QModelIndex &parent, int start, int end) { Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); if (!d->mShufflePlayList) { endRemoveRows(); } if (!d->mCurrentTrack.isValid()) { d->mCurrentTrack = index(d->mCurrentPlayListPosition, 0); if (d->mCurrentTrack.isValid()) { notifyCurrentTrackChanged(); } if (!d->mCurrentTrack.isValid()) { Q_EMIT playListFinished(); determineTracks(); if (!d->mCurrentTrack.isValid()) { notifyCurrentTrackChanged(); } } } if (!d->mNextTrack.isValid() || !d->mPreviousTrack.isValid()) { determineAndNotifyPreviousAndNextTracks(); } Q_EMIT tracksCountChanged(); Q_EMIT remainingTracksChanged(); Q_EMIT persistentStateChanged(); } void MediaPlayListProxyModel::sourceRowsAboutToBeMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destRow) { Q_ASSERT(!d->mShufflePlayList); beginMoveRows(parent, start, end, destParent, destRow); } void MediaPlayListProxyModel::sourceRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destRow) { Q_ASSERT(!d->mShufflePlayList); Q_UNUSED(parent); Q_UNUSED(start); Q_UNUSED(end); Q_UNUSED(destParent); Q_UNUSED(destRow); endMoveRows(); Q_EMIT remainingTracksChanged(); Q_EMIT persistentStateChanged(); } void MediaPlayListProxyModel::sourceModelAboutToBeReset() { beginResetModel(); } void MediaPlayListProxyModel::sourceModelReset() { endResetModel(); } void MediaPlayListProxyModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { auto startSourceRow = topLeft.row(); auto endSourceRow = bottomRight.row(); for (int i = startSourceRow; i <= endSourceRow; i++) { Q_EMIT dataChanged(index(mapRowFromSource(i), 0), index(mapRowFromSource(i), 0), roles); if (i == d->mCurrentTrack.row()) { Q_EMIT currentTrackDataChanged(); } else if (i == d->mNextTrack.row()) { Q_EMIT nextTrackDataChanged(); } else if (i == d->mPreviousTrack.row()) { Q_EMIT previousTrackDataChanged(); } determineTracks(); } } void MediaPlayListProxyModel::sourceLayoutAboutToBeChanged() { Q_EMIT layoutAboutToBeChanged(); } void MediaPlayListProxyModel::sourceLayoutChanged() { Q_EMIT layoutChanged(); } void MediaPlayListProxyModel::sourceHeaderDataChanged(Qt::Orientation orientation, int first, int last) { Q_EMIT headerDataChanged(orientation, first, last); } int MediaPlayListProxyModel::remainingTracks() const { if (!d->mCurrentTrack.isValid() || d->mRepeatPlay) { return -1; } else { return rowCount() - d->mCurrentTrack.row() - 1; } } int MediaPlayListProxyModel::tracksCount() const { return rowCount(); } int MediaPlayListProxyModel::currentTrackRow() const { return d->mCurrentTrack.row(); } void MediaPlayListProxyModel::enqueue(const DataTypes::MusicDataType &newEntry, const QString &newEntryTitle, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay) { enqueue({{newEntry, newEntryTitle, {}}}, enqueueMode, triggerPlay); } void MediaPlayListProxyModel::enqueue(const QUrl &entryUrl, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay) { enqueue({{{{DataTypes::ElementTypeRole, ElisaUtils::Track}}, {}, entryUrl}}, enqueueMode, triggerPlay); } void MediaPlayListProxyModel::enqueue(const DataTypes::EntryDataList &newEntries, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay) { if (newEntries.isEmpty()) { return; } d->mTriggerPlay = triggerPlay; if (enqueueMode == ElisaUtils::ReplacePlayList) { if (rowCount() == 0) { Q_EMIT hideUndoNotification(); } else { clearPlayList(); } } d->mPlayListModel->enqueueMultipleEntries(newEntries); } void MediaPlayListProxyModel::trackInError(const QUrl &sourceInError, QMediaPlayer::Error playerError) { d->mPlayListModel->trackInError(sourceInError, playerError); } void MediaPlayListProxyModel::skipNextTrack() { if (!d->mCurrentTrack.isValid()) { return; } if (d->mCurrentTrack.row() >= rowCount() - 1) { d->mCurrentTrack = index(0, 0); if (!d->mRepeatPlay) { Q_EMIT playListFinished(); } } else { d->mCurrentTrack = index(d->mCurrentTrack.row() + 1, 0); } notifyCurrentTrackChanged(); } void MediaPlayListProxyModel::skipPreviousTrack() { if (!d->mCurrentTrack.isValid()) { return; } if (d->mCurrentTrack.row() == 0) { if (d->mRepeatPlay) { d->mCurrentTrack = index(rowCount() - 1, 0); } else { return; } } else { d->mCurrentTrack = index(d->mCurrentTrack.row() - 1, 0); } notifyCurrentTrackChanged(); } void MediaPlayListProxyModel::switchTo(int row) { if (!d->mCurrentTrack.isValid()) { return; } d->mCurrentTrack = index(row, 0); notifyCurrentTrackChanged(); } void MediaPlayListProxyModel::removeSelection(QList selection) { std::sort(selection.begin(), selection.end()); std::reverse(selection.begin(), selection.end()); for (auto oneItem : selection) { removeRow(oneItem); } } void MediaPlayListProxyModel::removeRow(int row) { d->mPlayListModel->removeRows(mapRowToSource(row), 1); } void MediaPlayListProxyModel::moveRow(int from, int to) { if (d->mShufflePlayList) { beginMoveRows({}, from, from, {}, from < to ? to + 1 : to); d->mRandomMapping.move(from, to); endMoveRows(); } else { d->mPlayListModel->moveRows({}, from, 1, {}, from < to ? to + 1 : to); } } void MediaPlayListProxyModel::notifyCurrentTrackChanged() { if (d->mCurrentTrack.isValid()) { d->mCurrentPlayListPosition = d->mCurrentTrack.row(); } else { d->mCurrentPlayListPosition = -1; } determineAndNotifyPreviousAndNextTracks(); Q_EMIT currentTrackChanged(d->mCurrentTrack); Q_EMIT currentTrackRowChanged(); Q_EMIT remainingTracksChanged(); } void MediaPlayListProxyModel::determineAndNotifyPreviousAndNextTracks() { if (!d->mCurrentTrack.isValid()) { d->mPreviousTrack = QPersistentModelIndex(); d->mNextTrack = QPersistentModelIndex(); } auto mOldPreviousTrack = d->mPreviousTrack; auto mOldNextTrack = d->mNextTrack; if (d->mRepeatPlay) { // forward to end or begin when repeating if (d->mCurrentTrack.row() == 0) { d->mPreviousTrack = index(rowCount() - 1, 0); } else { d->mPreviousTrack = index(d->mCurrentTrack.row() - 1, 0); } if (d->mCurrentTrack.row() == rowCount() - 1) { d->mNextTrack = index(0, 0); } else { d->mNextTrack = index(d->mCurrentTrack.row() + 1, 0); } } else { // return nothing if no tracks available if (d->mCurrentTrack.row() == 0) { d->mPreviousTrack = QPersistentModelIndex(); } else { d->mPreviousTrack = index(d->mCurrentTrack.row() - 1, 0); } if (d->mCurrentTrack.row() == rowCount() - 1) { d->mNextTrack = QPersistentModelIndex(); } else { d->mNextTrack = index(d->mCurrentTrack.row() + 1, 0); } } if (d->mPreviousTrack != mOldPreviousTrack) { Q_EMIT previousTrackChanged(d->mPreviousTrack); } if (d->mNextTrack != mOldNextTrack) { Q_EMIT nextTrackChanged(d->mNextTrack); } } void MediaPlayListProxyModel::clearPlayList() { if (rowCount() == 0) { return; } d->mPersistentSettingsForUndo = persistentState(); d->mCurrentPlayListPosition = -1; d->mCurrentTrack = QPersistentModelIndex{}; d->mPlayListModel->clearPlayList(); Q_EMIT clearPlayListPlayer(); Q_EMIT displayUndoNotification(); } void MediaPlayListProxyModel::undoClearPlayList() { d->mPlayListModel->clearPlayList(); setPersistentState(d->mPersistentSettingsForUndo); Q_EMIT hideUndoNotification(); Q_EMIT undoClearPlayListPlayer(); } void MediaPlayListProxyModel::determineTracks() { if (!d->mCurrentTrack.isValid()) { for (int row = 0; row < rowCount(); ++row) { auto candidateTrack = index(row, 0); const auto type = candidateTrack.data(MediaPlayList::ElementTypeRole).value(); if (candidateTrack.isValid() && candidateTrack.data(MediaPlayList::IsValidRole).toBool() && (type == ElisaUtils::Track || type == ElisaUtils::Radio || type == ElisaUtils::FileName)) { d->mCurrentTrack = candidateTrack; notifyCurrentTrackChanged(); if (d->mTriggerPlay == ElisaUtils::TriggerPlay) { d->mTriggerPlay = ElisaUtils::DoNotTriggerPlay; Q_EMIT ensurePlay(); } break; } } } if (!d->mNextTrack.isValid() || !d->mPreviousTrack.isValid()) { determineAndNotifyPreviousAndNextTracks(); } } bool MediaPlayListProxyModel::savePlayList(const QUrl &fileName) { QMediaPlaylist savePlaylist; for (int i = 0; i < rowCount(); ++i) { if (data(index(i,0), MediaPlayList::IsValidRole).toBool()) { data(index(i,0), MediaPlayList::ResourceRole).toUrl(); savePlaylist.addMedia(data(index(i,0), MediaPlayList::ResourceRole).toUrl()); } } return savePlaylist.save(fileName, "m3u"); } void MediaPlayListProxyModel::loadPlayList(const QUrl &fileName) { d->mLoadPlaylist.clear(); d->mLoadPlaylist.load(fileName, "m3u"); } void MediaPlayListProxyModel::loadPlayListLoaded() { clearPlayList(); auto newTracks = DataTypes::EntryDataList{}; for (int i = 0; i < d->mLoadPlaylist.mediaCount(); ++i) { #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) newTracks.push_back({{{{DataTypes::ElementTypeRole, ElisaUtils::FileName}}}, {}, d->mLoadPlaylist.media(i).canonicalUrl()}); #else newTracks.push_back({{{{DataTypes::ElementTypeRole, ElisaUtils::FileName}}}, {}, d->mLoadPlaylist.media(i).request().url()}); #endif } enqueue(newTracks, ElisaUtils::ReplacePlayList, ElisaUtils::DoNotTriggerPlay); Q_EMIT persistentStateChanged(); d->mLoadPlaylist.clear(); Q_EMIT playListLoaded(); } void MediaPlayListProxyModel::loadPlayListLoadFailed() { d->mLoadPlaylist.clear(); Q_EMIT playListLoadFailed(); } QVariantMap MediaPlayListProxyModel::persistentState() const { QVariantMap currentState; currentState[QStringLiteral("playList")] = d->mPlayListModel->getEntriesForRestore(); currentState[QStringLiteral("currentTrack")] = d->mCurrentPlayListPosition; currentState[QStringLiteral("shufflePlayList")] = d->mShufflePlayList; currentState[QStringLiteral("repeatPlay")] = d->mRepeatPlay; return currentState; } void MediaPlayListProxyModel::setPersistentState(const QVariantMap &persistentStateValue) { qCDebug(orgKdeElisaPlayList()) << "MediaPlayListProxyModel::setPersistentState" << persistentStateValue; auto playListIt = persistentStateValue.find(QStringLiteral("playList")); if (playListIt != persistentStateValue.end()) { d->mPlayListModel->enqueueRestoredEntries(playListIt.value().toList()); } auto playerCurrentTrack = persistentStateValue.find(QStringLiteral("currentTrack")); if (playerCurrentTrack != persistentStateValue.end()) { auto newIndex = index(playerCurrentTrack->toInt(), 0); if (newIndex.isValid() && (newIndex != d->mCurrentTrack)) { d->mCurrentTrack = newIndex; notifyCurrentTrackChanged(); } } auto shufflePlayListStoredValue = persistentStateValue.find(QStringLiteral("shufflePlayList")); if (shufflePlayListStoredValue != persistentStateValue.end()) { setShufflePlayList(shufflePlayListStoredValue->toBool()); } auto repeatPlayStoredValue = persistentStateValue.find(QStringLiteral("repeatPlay")); if (repeatPlayStoredValue != persistentStateValue.end()) { setRepeatPlay(repeatPlayStoredValue->toBool()); } Q_EMIT persistentStateChanged(); } void MediaPlayListProxyModel::enqueueDirectory(const QUrl &fileName, ElisaUtils::PlayListEntryType databaseIdType, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay, int depth) { if (!fileName.isLocalFile()) return; // clear playlist if required if (enqueueMode == ElisaUtils::ReplacePlayList) { if (rowCount() == 0) { Q_EMIT hideUndoNotification(); } else { clearPlayList(); } } // get contents of directory QDir dirInfo = QDir(fileName.toLocalFile()); auto files = dirInfo.entryInfoList(QDir::NoDotAndDotDot | QDir::Readable | QDir::Files | QDir::Dirs, QDir::Name); auto newFiles = DataTypes::EntryDataList(); for (auto file : files) { auto fileUrl = QUrl::fromLocalFile(file.filePath()); if (file.isFile() && d->mMimeDb.mimeTypeForUrl(fileUrl).name().startsWith(QLatin1String("audio/"))) { - newFiles.append({DataTypes::EntryData{{},{},fileUrl}}); + newFiles.push_back({{{DataTypes::ElementTypeRole, ElisaUtils::Track}, + {DataTypes::ResourceRole, fileUrl}},{}, {}}); } else if (file.isDir() && depth > 1) { // recurse through directory enqueueDirectory(fileUrl, databaseIdType, ElisaUtils::AppendPlayList, triggerPlay, depth-1); } } if (newFiles.size() != 0) enqueue(newFiles, ElisaUtils::AppendPlayList, triggerPlay); } #include "moc_mediaplaylistproxymodel.cpp" diff --git a/src/models/filebrowserproxymodel.cpp b/src/models/filebrowserproxymodel.cpp index 6e30edc1..9f0ffe0c 100644 --- a/src/models/filebrowserproxymodel.cpp +++ b/src/models/filebrowserproxymodel.cpp @@ -1,249 +1,243 @@ /* SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien SPDX-FileCopyrightText: 2018 (c) Alexander Stippich SPDX-License-Identifier: LGPL-3.0-or-later */ #include "filebrowserproxymodel.h" #include "filebrowsermodel.h" #include "mediaplaylistproxymodel.h" #include #include #include #include #include "elisautils.h" FileBrowserProxyModel::FileBrowserProxyModel(QObject *parent) : KDirSortFilterProxyModel(parent) { setFilterCaseSensitivity(Qt::CaseInsensitive); mThreadPool.setMaxThreadCount(1); setSortFoldersFirst(true); sort(Qt::AscendingOrder); } FileBrowserProxyModel::~FileBrowserProxyModel() = default; QString FileBrowserProxyModel::filterText() const { return mFilterText; } void FileBrowserProxyModel::setFilterText(const QString &filterText) { QWriteLocker writeLocker(&mDataLock); if (mFilterText == filterText) return; mFilterText = filterText; mFilterExpression.setPattern(mFilterText); mFilterExpression.setPatternOptions(QRegularExpression::CaseInsensitiveOption); mFilterExpression.optimize(); invalidate(); Q_EMIT filterTextChanged(mFilterText); } bool FileBrowserProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { bool result = false; for (int column = 0, columnCount = sourceModel()->columnCount(source_parent); column < columnCount; ++column) { auto currentIndex = sourceModel()->index(source_row, column, source_parent); const auto &nameValue = sourceModel()->data(currentIndex, FileBrowserModel::NameRole).toString(); if (mFilterExpression.match(nameValue).hasMatch()) { result = true; continue; } if (result) { continue; } if (!result) { break; } } return result; } -void FileBrowserProxyModel::enqueueToPlayList() +void FileBrowserProxyModel::genericEnqueueToPlayList(ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay) { QtConcurrent::run(&mThreadPool, [=] () { QReadLocker locker(&mDataLock); auto allTrackUrls = DataTypes::EntryDataList{}; for (int rowIndex = 0, maxRowCount = rowCount(); rowIndex < maxRowCount; ++rowIndex) { auto currentIndex = index(rowIndex, 0); if (!data(currentIndex, FileBrowserModel::IsDirectoryRole).toBool()) { - allTrackUrls.push_back({{}, {}, data(currentIndex, FileBrowserModel::FileUrlRole).toUrl()}); + allTrackUrls.push_back({{{DataTypes::ElementTypeRole, ElisaUtils::Track}, + {DataTypes::ResourceRole, data(currentIndex, FileBrowserModel::FileUrlRole).toUrl()}}, {}, {}}); } } - Q_EMIT filesToEnqueue(allTrackUrls, - ElisaUtils::AppendPlayList, - ElisaUtils::DoNotTriggerPlay); + Q_EMIT entriesToEnqueue(allTrackUrls, enqueueMode, triggerPlay); }); } +void FileBrowserProxyModel::enqueueToPlayList() +{ + genericEnqueueToPlayList(ElisaUtils::AppendPlayList, + ElisaUtils::DoNotTriggerPlay); +} + void FileBrowserProxyModel::replaceAndPlayOfPlayList() { - QtConcurrent::run(&mThreadPool, [=] () { - QReadLocker locker(&mDataLock); - auto allTrackUrls = DataTypes::EntryDataList{}; - for (int rowIndex = 0, maxRowCount = rowCount(); rowIndex < maxRowCount; ++rowIndex) { - auto currentIndex = index(rowIndex, 0); - if (!data(currentIndex, FileBrowserModel::IsDirectoryRole).toBool()) { - allTrackUrls.push_back({{}, {}, data(currentIndex, FileBrowserModel::FileUrlRole).toUrl()}); - } - } - Q_EMIT filesToEnqueue(allTrackUrls, - ElisaUtils::ReplacePlayList, + genericEnqueueToPlayList(ElisaUtils::ReplacePlayList, ElisaUtils::TriggerPlay); - }); } QString FileBrowserProxyModel::parentFolder() const { auto fileBrowserModel = dynamic_cast(sourceModel()); if (!fileBrowserModel) { return {}; } //return to the top folder if parent directory does not exist QDir dir(fileBrowserModel->dirLister()->url().toLocalFile()); if (dir.cdUp()) { return dir.path(); } else { return mTopFolder; } } void FileBrowserProxyModel::disconnectPlayList() { if (mPlayList) { - disconnect(this, &FileBrowserProxyModel::filesToEnqueue, + disconnect(this, &FileBrowserProxyModel::entriesToEnqueue, mPlayList, static_cast(&MediaPlayListProxyModel::enqueue)); } } void FileBrowserProxyModel::connectPlayList() { if (mPlayList) { - connect(this, &FileBrowserProxyModel::filesToEnqueue, + connect(this, &FileBrowserProxyModel::entriesToEnqueue, mPlayList, static_cast(&MediaPlayListProxyModel::enqueue)); } } void FileBrowserProxyModel::openParentFolder() { auto fileBrowserModel = dynamic_cast(sourceModel()); if (!fileBrowserModel) { return; } if (canGoBack()) { QString parent = parentFolder(); fileBrowserModel->setUrl(parent); if (parent == mTopFolder) { Q_EMIT canGoBackChanged(); } } } bool FileBrowserProxyModel::canGoBack() const { auto fileBrowserModel = dynamic_cast(sourceModel()); if (!fileBrowserModel) { return false; } return fileBrowserModel->dirLister()->url().toLocalFile() != mTopFolder; } void FileBrowserProxyModel::openFolder(const QString &folder, bool isDisplayRoot) { auto fileBrowserModel = dynamic_cast(sourceModel()); if (!fileBrowserModel) { return; } if (folder.isEmpty()) { return; } fileBrowserModel->setUrl(folder); if (!isDisplayRoot) { Q_EMIT canGoBackChanged(); } } QString FileBrowserProxyModel::url() const { auto fileBrowserModel = dynamic_cast(sourceModel()); if (!fileBrowserModel) { return {}; } return fileBrowserModel->dirLister()->url().toLocalFile(); } bool FileBrowserProxyModel::sortedAscending() const { return sortOrder() ? false : true; } void FileBrowserProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { KDirSortFilterProxyModel::setSourceModel(sourceModel); auto fileBrowserModel = dynamic_cast(sourceModel); if (!fileBrowserModel) { return; } connect(fileBrowserModel, &FileBrowserModel::urlChanged,this, &FileBrowserProxyModel::urlChanged); mTopFolder = QDir::homePath(); openFolder(mTopFolder, true); } MediaPlayListProxyModel *FileBrowserProxyModel::playList() const { return mPlayList; } void FileBrowserProxyModel::sortModel(Qt::SortOrder order) { this->sort(0,order); Q_EMIT sortedAscendingChanged(); } void FileBrowserProxyModel::setPlayList(MediaPlayListProxyModel *playList) { disconnectPlayList(); if (mPlayList == playList) { return; } mPlayList = playList; Q_EMIT playListChanged(); connectPlayList(); } #include "moc_filebrowserproxymodel.cpp" diff --git a/src/models/filebrowserproxymodel.h b/src/models/filebrowserproxymodel.h index ea42de32..4dbdd17d 100644 --- a/src/models/filebrowserproxymodel.h +++ b/src/models/filebrowserproxymodel.h @@ -1,127 +1,130 @@ /* SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien SPDX-FileCopyrightText: 2018 (c) Alexander Stippich SPDX-License-Identifier: LGPL-3.0-or-later */ #ifndef FILEBROWSERPROXYMODEL_H #define FILEBROWSERPROXYMODEL_H #include "elisaLib_export.h" #include "filescanner.h" #include "elisautils.h" #include #include #include #include #include class MediaPlayListProxyModel; class ELISALIB_EXPORT FileBrowserProxyModel : public KDirSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QString filterText READ filterText WRITE setFilterText NOTIFY filterTextChanged) Q_PROPERTY(bool canGoBack READ canGoBack NOTIFY canGoBackChanged) Q_PROPERTY(QString url READ url NOTIFY urlChanged) Q_PROPERTY(bool sortedAscending READ sortedAscending NOTIFY sortedAscendingChanged) Q_PROPERTY(MediaPlayListProxyModel* playList READ playList WRITE setPlayList NOTIFY playListChanged) public: explicit FileBrowserProxyModel(QObject *parent = nullptr); ~FileBrowserProxyModel() override; QString filterText() const; QString url() const; bool canGoBack() const; bool sortedAscending() const; void setSourceModel(QAbstractItemModel *sourceModel) override; MediaPlayListProxyModel* playList() const; public Q_SLOTS: void enqueueToPlayList(); void replaceAndPlayOfPlayList(); void setFilterText(const QString &filterText); void openParentFolder(); void openFolder(const QString &folder, bool isDisplayRoot = false); void sortModel(Qt::SortOrder order); void setPlayList(MediaPlayListProxyModel* playList); Q_SIGNALS: - void filesToEnqueue(const DataTypes::EntryDataList &newFiles, - ElisaUtils::PlayListEnqueueMode enqueueMode, - ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay); + void entriesToEnqueue(const DataTypes::EntryDataList &newEntries, + ElisaUtils::PlayListEnqueueMode enqueueMode, + ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay); void urlChanged(); void canGoBackChanged(); void filterTextChanged(const QString &filterText); void sortedAscendingChanged(); void playListChanged(); protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; private: + void genericEnqueueToPlayList(ElisaUtils::PlayListEnqueueMode enqueueMode, + ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay); + QString parentFolder() const; void disconnectPlayList(); void connectPlayList(); QString mTopFolder; FileScanner mFileScanner; QString mFilterText; QRegularExpression mFilterExpression; QReadWriteLock mDataLock; QThreadPool mThreadPool; MediaPlayListProxyModel* mPlayList = nullptr; }; #endif // FILEBROWSERPROXYMODEL_H diff --git a/src/qml/FileBrowserView.qml b/src/qml/FileBrowserView.qml index 7ea9502c..660d8c9a 100644 --- a/src/qml/FileBrowserView.qml +++ b/src/qml/FileBrowserView.qml @@ -1,172 +1,180 @@ /* SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien SPDX-FileCopyrightText: 2018 (c) Alexander Stippich SPDX-License-Identifier: LGPL-3.0-or-later */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQml.Models 2.2 import QtQuick.Layouts 1.2 import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { id: fileView property bool isSubPage: false property alias expandedFilterView: navigationBar.expandedFilterView function goBack() { proxyModel.openParentFolder() } function loadFolderAndClear(data) { proxyModel.openFolder(data) navigationBar.filterText = "" } FileBrowserModel { id: realModel } FileBrowserProxyModel { id: proxyModel sourceModel: realModel playList: elisa.mediaPlayListProxyModel } MouseArea { anchors.fill: parent hoverEnabled: false acceptedButtons: Qt.BackButton onClicked: proxyModel.openParentFolder() } ColumnLayout { anchors.fill: parent spacing: 0 NavigationActionBar { id: navigationBar mainTitle: i18nc("Title of the file browser view", "Files") image: elisaTheme.folderIcon secondaryTitle: proxyModel.url enableGoBack: proxyModel.canGoBack sortOrder: proxyModel.sortedAscending showRating: false Layout.fillWidth: true Binding { target: proxyModel property: 'filterText' value: navigationBar.filterText } onEnqueue: proxyModel.enqueueToPlayList() onReplaceAndPlay: proxyModel.replaceAndPlayOfPlayList() onGoBack: proxyModel.openParentFolder() onSort: proxyModel.sortModel(order) } Rectangle { color: myPalette.base Layout.fillHeight: true Layout.fillWidth: true clip: true GridView { id: contentDirectoryView anchors.fill: parent activeFocusOnTab: true keyNavigationEnabled: true ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds currentIndex: -1 Accessible.role: Accessible.List Accessible.name: proxyModel.url model: proxyModel ScrollHelper { id: scrollHelper flickable: contentDirectoryView anchors.fill: contentDirectoryView } add: Transition { PropertyAnimation { property: "opacity" from: 0 to: 1 duration: Kirigami.Units.shortDuration } } remove: Transition { PropertyAnimation { property: "opacity" from: 0 to: 1 duration: Kirigami.Units.shortDuration } } cellWidth: Math.floor(availableWidth / Math.max(Math.floor(availableWidth / elisaTheme.gridDelegateSize), 2)) cellHeight: elisaTheme.gridDelegateSize + Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing delegate: GridBrowserDelegate { width: elisaTheme.gridDelegateSize height: contentDirectoryView.cellHeight focus: true isSelected: contentDirectoryView.currentIndex === index mainText: model.name delegateDisplaySecondaryText: false fileUrl: model.fileUrl entryType: ElisaUtils.FileName imageUrl: model.imageUrl showDetailsButton: !model.isDirectory && !model.isPlaylist showEnqueueButton: !model.isPlaylist showPlayButton: true - onEnqueue: isDirectory ? elisa.mediaPlayListProxyModel.enqueueDirectory(url, ElisaUtils.FileName, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay, 10) : elisa.mediaPlayListProxyModel.enqueue(url, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) + onEnqueue: { + if (model.isDirectory) { + elisa.mediaPlayListProxyModel.enqueueDirectory(model.fileUrl, ElisaUtils.FileName, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay, 10) + } else if (model.isPlaylist) { + elisa.mediaPlayListProxyModel.loadPlayList(model.fileUrl) + } else { + elisa.mediaPlayListProxyModel.enqueue(model.fileUrl, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) + } + } onReplaceAndPlay: { if (model.isDirectory) { - elisa.mediaPlayListProxyModel.enqueueDirectory(url, ElisaUtils.FileName, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay, 10) + elisa.mediaPlayListProxyModel.enqueueDirectory(model.fileUrl, ElisaUtils.FileName, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay, 10) } else if (model.isPlaylist) { - elisa.mediaPlayListProxyModel.loadPlaylist(url) + elisa.mediaPlayListProxyModel.loadPlayList(model.fileUrl) } else { - elisa.mediaPlayListProxyModel.enqueue(url, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) + elisa.mediaPlayListProxyModel.enqueue(model.fileUrl, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) } } onSelected: { forceActiveFocus() contentDirectoryView.currentIndex = model.index } onActiveFocusChanged: { if (activeFocus && contentDirectoryView.currentIndex !== model.index) { contentDirectoryView.currentIndex = model.index } } onOpen: isDirectory ? loadFolderAndClear(model.fileUrl) : elisa.mediaPlayListProxyModel.enqueue(model.fileUrl, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) } } } } }