diff --git a/src/mediaplaylistproxymodel.cpp b/src/mediaplaylistproxymodel.cpp index 198e04a4..77795e2e 100644 --- a/src/mediaplaylistproxymodel.cpp +++ b/src/mediaplaylistproxymodel.cpp @@ -1,808 +1,805 @@ /* 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(qulonglong newEntryDatabaseId, - const QString &newEntryTitle, - ElisaUtils::PlayListEnqueueMode enqueueMode, - ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay) +void MediaPlayListProxyModel::enqueue(const DataTypes::MusicDataType &newEntry, const QString &newEntryTitle, + ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay) { - enqueue({{{{DataTypes::ElementTypeRole, ElisaUtils::Track}, {DataTypes::DatabaseIdRole, newEntryDatabaseId}}, newEntryTitle, {}}}, - enqueueMode, 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}}); } 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/mediaplaylistproxymodel.h b/src/mediaplaylistproxymodel.h index 2739bea7..4fea6241 100644 --- a/src/mediaplaylistproxymodel.h +++ b/src/mediaplaylistproxymodel.h @@ -1,249 +1,250 @@ /* SPDX-FileCopyrightText: 2015 (c) Matthieu Gallien SPDX-FileCopyrightText: 2019 (c) Alexander Stippich SPDX-License-Identifier: LGPL-3.0-or-later */ #ifndef MEDIAPLAYLISTPROXYMODEL_H #define MEDIAPLAYLISTPROXYMODEL_H #include "elisaLib_export.h" #include "elisautils.h" #include "datatypes.h" #include #include #include class MediaPlayList; class MediaPlayListProxyModelPrivate; class ELISALIB_EXPORT MediaPlayListProxyModel : public QAbstractProxyModel { Q_OBJECT Q_PROPERTY(QVariantMap persistentState READ persistentState WRITE setPersistentState NOTIFY persistentStateChanged) Q_PROPERTY(QPersistentModelIndex previousTrack READ previousTrack NOTIFY previousTrackChanged) Q_PROPERTY(QPersistentModelIndex currentTrack READ currentTrack NOTIFY currentTrackChanged) Q_PROPERTY(QPersistentModelIndex nextTrack READ nextTrack NOTIFY nextTrackChanged) Q_PROPERTY(bool repeatPlay READ repeatPlay WRITE setRepeatPlay NOTIFY repeatPlayChanged) Q_PROPERTY(bool shufflePlayList READ shufflePlayList WRITE setShufflePlayList NOTIFY shufflePlayListChanged) Q_PROPERTY(int remainingTracks READ remainingTracks NOTIFY remainingTracksChanged) Q_PROPERTY(int currentTrackRow READ currentTrackRow NOTIFY currentTrackRowChanged) Q_PROPERTY(int tracksCount READ tracksCount NOTIFY tracksCountChanged) public: explicit MediaPlayListProxyModel(QObject *parent = nullptr); ~MediaPlayListProxyModel() override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; QItemSelection mapSelectionFromSource(const QItemSelection &sourceSelection) const override; QItemSelection mapSelectionToSource(const QItemSelection &proxySelection) const override; QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; int mapRowFromSource(const int sourceRow) const; int mapRowToSource(const int proxyRow) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent) const override; QModelIndex parent(const QModelIndex &child) const override; bool hasChildren(const QModelIndex &parent) const override; void setPlayListModel(MediaPlayList* playListModel); QPersistentModelIndex previousTrack() const; QPersistentModelIndex currentTrack() const; QPersistentModelIndex nextTrack() const; bool repeatPlay() const; bool shufflePlayList() const; int remainingTracks() const; int currentTrackRow() const; int tracksCount() const; QVariantMap persistentState() const; public Q_SLOTS: void enqueue(const QUrl &entryUrl, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay); - void enqueue(qulonglong newEntryDatabaseId, const QString &newEntryTitle, + void enqueue(const DataTypes::MusicDataType &newEntry, + const QString &newEntryTitle, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay); void enqueue(const DataTypes::EntryDataList &newEntries, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay); void setRepeatPlay(bool value); void setShufflePlayList(bool value); void trackInError(const QUrl &sourceInError, QMediaPlayer::Error playerError); void skipNextTrack(); void skipPreviousTrack(); void switchTo(int row); void removeSelection(QList selection); void removeRow(int row); void moveRow(int from, int to); void clearPlayList(); void undoClearPlayList(); bool savePlayList(const QUrl &fileName); void loadPlayList(const QUrl &fileName); void setPersistentState(const QVariantMap &persistentState); void enqueueDirectory(const QUrl &fileName, ElisaUtils::PlayListEntryType databaseIdType, ElisaUtils::PlayListEnqueueMode enqueueMode, ElisaUtils::PlayListEnqueueTriggerPlay triggerPlay, int depth); Q_SIGNALS: void previousTrackChanged(const QPersistentModelIndex &previousTrack); void currentTrackChanged(const QPersistentModelIndex ¤tTrack); void nextTrackChanged(const QPersistentModelIndex &nextTrack); void previousTrackDataChanged(); void currentTrackDataChanged(); void nextTrackDataChanged(); void repeatPlayChanged(); void shufflePlayListChanged(); void remainingTracksChanged(); void ensurePlay(); void currentTrackRowChanged(); void tracksCountChanged(); void playListFinished(); void playListLoaded(); void playListLoadFailed(); void persistentStateChanged(); void clearPlayListPlayer(); void undoClearPlayListPlayer(); void displayUndoNotification(); void hideUndoNotification(); private Q_SLOTS: void sourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); void sourceRowsInserted(const QModelIndex &parent, int start, int end); void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); void sourceRowsRemoved(const QModelIndex &parent, int start, int end); void sourceRowsAboutToBeMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destRow); void sourceRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destParent, int destRow); void sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); void sourceHeaderDataChanged(Qt::Orientation orientation, int first, int last); void sourceLayoutAboutToBeChanged(); void sourceLayoutChanged(); void sourceModelAboutToBeReset(); void sourceModelReset(); private Q_SLOTS: void loadPlayListLoaded(); void loadPlayListLoadFailed(); private: void setSourceModel(QAbstractItemModel *sourceModel) override; void determineTracks(); void notifyCurrentTrackChanged(); void determineAndNotifyPreviousAndNextTracks(); std::unique_ptr d; }; #endif // MEDIAPLAYLISTPROXYMODEL_H diff --git a/src/qml/DataGridView.qml b/src/qml/DataGridView.qml index 5454c127..b582d1f5 100644 --- a/src/qml/DataGridView.qml +++ b/src/qml/DataGridView.qml @@ -1,96 +1,96 @@ /* SPDX-FileCopyrightText: 2018 (c) Matthieu Gallien SPDX-License-Identifier: LGPL-3.0-or-later */ import QtQuick 2.10 import QtQuick.Controls 2.3 import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { id: viewHeader property var filterType property alias mainTitle: gridView.mainTitle property alias secondaryTitle: gridView.secondaryTitle property alias image: gridView.image property var modelType property alias defaultIcon: gridView.defaultIcon property alias showRating: gridView.showRating property alias delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText property alias isSubPage: gridView.isSubPage property alias expandedFilterView: gridView.expandedFilterView property string genreFilterText property string artistFilter focus: true Accessible.role: Accessible.Pane Accessible.name: mainTitle function initializeModel() { realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, filterType, genreFilterText, artistFilter, 0) } DataModel { id: realModel } GridViewProxyModel { id: proxyModel sourceModel: realModel playList: elisa.mediaPlayListProxyModel } GridBrowserView { id: gridView focus: true anchors.fill: parent contentModel: proxyModel - onEnqueue: elisa.mediaPlayListProxyModel.enqueue(databaseId, name, modelType, + onEnqueue: elisa.mediaPlayListProxyModel.enqueue(fullData, name, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) - onReplaceAndPlay: elisa.mediaPlayListProxyModel.enqueue(databaseId, name, modelType, + onReplaceAndPlay: elisa.mediaPlayListProxyModel.enqueue(fullData, name, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) onOpen: viewManager.openChildView(innerMainTitle, innerSecondaryTitle, innerImage, databaseId, dataType) onGoBack: viewManager.goBack() Loader { anchors.centerIn: parent height: Kirigami.Units.gridUnit * 5 width: height visible: realModel.isBusy active: realModel.isBusy sourceComponent: BusyIndicator { anchors.centerIn: parent } } } Connections { target: elisa onMusicManagerChanged: initializeModel() } Component.onCompleted: { if (elisa.musicManager) { initializeModel() } } } diff --git a/src/qml/DataListView.qml b/src/qml/DataListView.qml index bbc79100..601f1717 100644 --- a/src/qml/DataListView.qml +++ b/src/qml/DataListView.qml @@ -1,229 +1,233 @@ /* SPDX-FileCopyrightText: 2018 (c) Matthieu Gallien SPDX-License-Identifier: LGPL-3.0-or-later */ import QtQuick 2.10 import QtQuick.Controls 2.3 import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { id: viewHeader property var filterType property alias isSubPage: listView.isSubPage property alias mainTitle: listView.mainTitle property alias secondaryTitle: listView.secondaryTitle property int databaseId property alias showSection: listView.showSection property alias expandedFilterView: listView.expandedFilterView property alias image: listView.image property var modelType property alias sortRole: proxyModel.sortRole property var sortAscending property bool displaySingleAlbum: false property alias radioCase: listView.showCreateRadioButton function openMetaDataView(databaseId, url, entryType) { metadataLoader.setSource("MediaTrackMetadataView.qml", { "fileName": url, "modelType": entryType, "showImage": entryType !== ElisaUtils.Radio, "showTrackFileName": entryType !== ElisaUtils.Radio, "showDeleteButton": entryType === ElisaUtils.Radio, "showApplyButton": entryType === ElisaUtils.Radio, "editableMetadata": entryType === ElisaUtils.Radio, "widthIndex": (entryType === ElisaUtils.Radio ? 4.5 : 2.8), }); metadataLoader.active = true } function openCreateRadioView() { metadataLoader.setSource("MediaTrackMetadataView.qml", { "modelType": ElisaUtils.Radio, "isCreation": true, "showImage": false, "showTrackFileName": false, "showDeleteButton": false, "showApplyButton": true, "editableMetadata": true, "widthIndex": 4.5, }); metadataLoader.active = true } DataModel { id: realModel } AllTracksProxyModel { id: proxyModel sourceModel: realModel playList: elisa.mediaPlayListProxyModel } Loader { id: metadataLoader active: false onLoaded: item.show() } Component { id: albumDelegate ListBrowserDelegate { id: entry width: listView.delegateWidth focus: true trackUrl: model.url dataType: model.dataType title: model.display ? model.display : '' artist: model.artist ? model.artist : '' album: model.album ? model.album : '' albumArtist: model.albumArtist ? model.albumArtist : '' duration: model.duration ? model.duration : '' imageUrl: model.imageUrl ? model.imageUrl : '' trackNumber: model.trackNumber ? model.trackNumber : -1 discNumber: model.discNumber ? model.discNumber : -1 rating: model.rating isSelected: listView.currentIndex === index isAlternateColor: (index % 2) === 1 detailedView: false - onEnqueue: elisa.mediaPlayListProxyModel.enqueue(url, ElisaUtils.AppendPlayList, + onEnqueue: elisa.mediaPlayListProxyModel.enqueue(model.fullData, model.display, + ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) - onReplaceAndPlay: elisa.mediaPlayListProxyModel.enqueue(url, ElisaUtils.ReplacePlayList, + onReplaceAndPlay: elisa.mediaPlayListProxyModel.enqueue(model.fullData, model.display, + ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) onClicked: listView.currentIndex = index onActiveFocusChanged: { if (activeFocus && listView.currentIndex !== index) { listView.currentIndex = index } } onCallOpenMetaDataView: { openMetaDataView(databaseId, url, entryType) } } } Component { id: detailedTrackDelegate ListBrowserDelegate { id: entry width: listView.delegateWidth focus: true trackUrl: model.url dataType: model.dataType title: model.display ? model.display : '' artist: model.artist ? model.artist : '' album: model.album ? model.album : '' albumArtist: model.albumArtist ? model.albumArtist : '' duration: model.duration ? model.duration : '' imageUrl: model.imageUrl ? model.imageUrl : '' trackNumber: model.trackNumber ? model.trackNumber : -1 discNumber: model.discNumber ? model.discNumber : -1 rating: model.rating hideDiscNumber: model.isSingleDiscAlbum isSelected: listView.currentIndex === index isAlternateColor: (index % 2) === 1 - onEnqueue: elisa.mediaPlayListProxyModel.enqueue(url, ElisaUtils.AppendPlayList, + onEnqueue: elisa.mediaPlayListProxyModel.enqueue(model.fullData, model.display, + ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) - onReplaceAndPlay: elisa.mediaPlayListProxyModel.enqueue(url, ElisaUtils.ReplacePlayList, + onReplaceAndPlay: elisa.mediaPlayListProxyModel.enqueue(model.fullData, model.display, + ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) onClicked: { listView.currentIndex = index entry.forceActiveFocus() } onCallOpenMetaDataView: { openMetaDataView(databaseId, url, entryType) } } } ListBrowserView { id: listView focus: true anchors.fill: parent contentModel: proxyModel delegate: (displaySingleAlbum ? albumDelegate : detailedTrackDelegate) enableSorting: !displaySingleAlbum allowArtistNavigation: isSubPage showCreateRadioButton: modelType === ElisaUtils.Radio showEnqueueButton: modelType !== ElisaUtils.Radio onShowArtist: { viewManager.openChildView(secondaryTitle, '', elisaTheme.artistIcon, 0, ElisaUtils.Artist) } onGoBack: viewManager.goBack() Loader { anchors.centerIn: parent height: Kirigami.Units.gridUnit * 5 width: height visible: realModel.isBusy active: realModel.isBusy sourceComponent: BusyIndicator { anchors.centerIn: parent } } } Connections { target: elisa onMusicManagerChanged: realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, modelType) } Connections { target: listView.navigationBar onCreateRadio: { openCreateRadioView() } } Component.onCompleted: { if (elisa.musicManager) { realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, filterType, mainTitle, secondaryTitle, databaseId) } if (sortAscending === ViewManager.SortAscending) { proxyModel.sortModel(Qt.AscendingOrder) } else if (sortAscending === ViewManager.SortDescending) { proxyModel.sortModel(Qt.DescendingOrder) } } } diff --git a/src/qml/GridBrowserDelegate.qml b/src/qml/GridBrowserDelegate.qml index 30ecae19..45081ffb 100644 --- a/src/qml/GridBrowserDelegate.qml +++ b/src/qml/GridBrowserDelegate.qml @@ -1,396 +1,396 @@ /* SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien SPDX-License-Identifier: LGPL-3.0-or-later */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import QtQml.Models 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.5 as Kirigami FocusScope { id: gridEntry property url imageUrl property url imageFallbackUrl property url fileUrl property var entryType property alias mainText: mainLabel.text property alias secondaryText: secondaryLabel.text property var databaseId property bool delegateDisplaySecondaryText: true property bool isPartial property bool isSelected property bool showDetailsButton: false property bool showPlayButton: true property bool showEnqueueButton: true - signal enqueue(var databaseId, var name, var url) - signal replaceAndPlay(var databaseId, var name, var url) + signal enqueue() + signal replaceAndPlay() signal open() signal selected() Loader { id: metadataLoader active: false && gridEntry.fileUrl onLoaded: item.show() sourceComponent: MediaTrackMetadataView { fileName: gridEntry.fileUrl ? gridEntry.fileUrl : '' showImage: true modelType: gridEntry.entryType showTrackFileName: true showDeleteButton: false showApplyButton: false editableMetadata: false onRejected: metadataLoader.active = false; } } Keys.onReturnPressed: open() Keys.onEnterPressed: open() Accessible.role: Accessible.ListItem Accessible.name: mainText Rectangle { id: stateIndicator anchors.fill: parent z: 1 color: "transparent" opacity: 0.4 radius: 3 } MouseArea { id: hoverHandle anchors.fill: parent z: 2 hoverEnabled: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor Layout.preferredHeight: gridEntry.height Layout.fillWidth: true onClicked: open() TextMetrics { id: mainLabelSize font: mainLabel.font text: mainLabel.text } ColumnLayout { id: mainData spacing: 0 anchors.fill: parent Item { Layout.margins: Kirigami.Units.largeSpacing Layout.preferredHeight: gridEntry.width - 2 * Kirigami.Units.largeSpacing Layout.preferredWidth: gridEntry.width - 2 * Kirigami.Units.largeSpacing Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Loader { id: hoverLoader active: false anchors { bottom: parent.bottom bottomMargin: 2 left: parent.left leftMargin: 2 } z: 1 opacity: 0 sourceComponent: Row { spacing: 2 Button { id: detailsButton objectName: 'detailsButton' icon.name: 'help-about' hoverEnabled: true ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: i18nc("Show track metadata", "View Details") Accessible.role: Accessible.Button Accessible.name: ToolTip.text Accessible.description: ToolTip.text Accessible.onPressAction: clicked() onClicked: { if (metadataLoader.active === false) { metadataLoader.active = true } else { metadataLoader.item.close(); metadataLoader.active = false } } Keys.onReturnPressed: clicked() Keys.onEnterPressed: clicked() visible: showDetailsButton width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize } Button { id: replaceAndPlayButton objectName: 'replaceAndPlayButton' icon.name: 'media-playback-start' hoverEnabled: true ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: i18nc("Clear play list and add whole container to play list", "Play now, replacing current playlist") Accessible.role: Accessible.Button Accessible.name: ToolTip.text Accessible.description: ToolTip.text Accessible.onPressAction: onClicked - onClicked: replaceAndPlay(databaseId, mainText, fileUrl) - Keys.onReturnPressed: replaceAndPlay(databaseId, mainText, fileUrl) - Keys.onEnterPressed: replaceAndPlay(databaseId, mainText, fileUrl) + onClicked: replaceAndPlay() + Keys.onReturnPressed: replaceAndPlay() + Keys.onEnterPressed: replaceAndPlay() visible: showPlayButton width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize } Button { id: enqueueButton objectName: 'enqueueButton' icon.name: 'list-add' hoverEnabled: true ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: i18nc("Add whole container to play list", "Add to playlist") Accessible.role: Accessible.Button Accessible.name: ToolTip.text Accessible.description: ToolTip.text Accessible.onPressAction: onClicked - onClicked: enqueue(databaseId, mainText, fileUrl) - Keys.onReturnPressed: enqueue(databaseId, mainText, fileUrl) - Keys.onEnterPressed: enqueue(databaseId, mainText, fileUrl) + onClicked: enqueue() + Keys.onReturnPressed: enqueue() + Keys.onEnterPressed: enqueue() visible: showEnqueueButton width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize } } } Loader { id: coverImageLoader active: !isPartial anchors.fill: parent sourceComponent: ImageWithFallback { id: coverImage anchors.fill: parent sourceSize.width: parent.width sourceSize.height: parent.height fillMode: Image.PreserveAspectFit smooth: true source: gridEntry.imageUrl fallback: gridEntry.imageFallbackUrl asynchronous: true layer.enabled: !coverImage.usingFallback layer.effect: DropShadow { source: coverImage radius: 10 spread: 0.1 samples: 21 color: myPalette.shadow } } } Loader { active: isPartial anchors.centerIn: parent height: Kirigami.Units.gridUnit * 5 width: height sourceComponent: BusyIndicator { anchors.centerIn: parent running: true } } } LabelWithToolTip { id: mainLabel level: 4 color: myPalette.text // FIXME: Center-aligned text looks better overall, but // sometimes results in font kerning issues // See https://bugreports.qt.io/browse/QTBUG-49646 horizontalAlignment: Text.AlignHCenter Layout.maximumWidth: gridEntry.width * 0.9 Layout.minimumWidth: Layout.maximumWidth Layout.maximumHeight: delegateDisplaySecondaryText ? mainLabelSize.boundingRect.height : mainLabelSize.boundingRect.height * 2 Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Layout.bottomMargin: delegateDisplaySecondaryText ? 0 : Kirigami.Units.smallSpacing wrapMode: delegateDisplaySecondaryText ? Label.NoWrap : Label.Wrap maximumLineCount: 2 elide: Text.ElideRight } LabelWithToolTip { id: secondaryLabel opacity: 0.6 color: myPalette.text // FIXME: Center-aligned text looks better overall, but // sometimes results in font kerning issues // See https://bugreports.qt.io/browse/QTBUG-49646 horizontalAlignment: Text.AlignHCenter Layout.bottomMargin: Kirigami.Units.smallSpacing Layout.maximumWidth: gridEntry.width * 0.9 Layout.minimumWidth: Layout.maximumWidth Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom visible: delegateDisplaySecondaryText elide: Text.ElideRight } Item { Layout.fillHeight: true } } } states: [ State { name: 'notSelected' when: !gridEntry.activeFocus && !hoverHandle.containsMouse && !gridEntry.isSelected PropertyChanges { target: stateIndicator color: 'transparent' } PropertyChanges { target: stateIndicator opacity: 1.0 } PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } }, State { name: 'hovered' when: hoverHandle.containsMouse && !gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.highlight } PropertyChanges { target: stateIndicator opacity: 0.2 } PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } }, State { name: 'selected' when: gridEntry.isSelected && !gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.mid } PropertyChanges { target: stateIndicator opacity: 0.6 } PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0. } }, State { name: 'hoveredOrSelected' when: gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.highlight } PropertyChanges { target: stateIndicator opacity: 0.6 } PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } } ] } diff --git a/src/qml/GridBrowserView.qml b/src/qml/GridBrowserView.qml index 3524cfb7..72b14d47 100644 --- a/src/qml/GridBrowserView.qml +++ b/src/qml/GridBrowserView.qml @@ -1,156 +1,156 @@ /* SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien SPDX-License-Identifier: LGPL-3.0-or-later */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import QtQml.Models 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { id: gridView property bool isSubPage: false property string mainTitle property string secondaryTitle property url image property alias contentModel: contentDirectoryView.model property alias showRating: navigationBar.showRating property bool delegateDisplaySecondaryText: true property alias expandedFilterView: navigationBar.expandedFilterView property var stackView property url defaultIcon - signal enqueue(int databaseId, string name) - signal replaceAndPlay(int databaseId, string name) - signal open(string innerMainTitle, string innerSecondaryTitle, url innerImage, int databaseId, var dataType) + signal enqueue(var fullData, string name) + signal replaceAndPlay(var fullData, string name) + signal open(string innerMainTitle, string innerSecondaryTitle, url innerImage, int databaseId, var dataType, var showDiscHeader) signal goBack() ColumnLayout { anchors.fill: parent spacing: 0 NavigationActionBar { id: navigationBar mainTitle: gridView.mainTitle secondaryTitle: gridView.secondaryTitle image: gridView.image enableGoBack: isSubPage sortOrder: if (contentModel) {contentModel.sortedAscending} else true Layout.fillWidth: true Loader { active: contentModel !== undefined sourceComponent: Binding { target: contentModel property: 'filterText' value: navigationBar.filterText } } Loader { active: contentModel sourceComponent: Binding { target: contentModel property: 'filterRating' value: navigationBar.filterRating } } onEnqueue: contentModel.enqueueToPlayList() onReplaceAndPlay:contentModel.replaceAndPlayOfPlayList() onGoBack: gridView.goBack() onSort: contentModel.sortModel(order) } FocusScope { Layout.fillHeight: true Layout.fillWidth: true clip: true GridView { id: contentDirectoryView property int availableWidth: scrollBar.visible ? width - scrollBar.width : width activeFocusOnTab: true keyNavigationEnabled: true anchors.fill: parent anchors.leftMargin: (LayoutMirroring.enabled && scrollBar.visible) ? 0 : Kirigami.Units.largeSpacing anchors.rightMargin: (!LayoutMirroring.enabled && scrollBar.visible) ? 0 : Kirigami.Units.largeSpacing anchors.topMargin: Kirigami.Units.largeSpacing anchors.bottomMargin: Kirigami.Units.largeSpacing ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds currentIndex: -1 Accessible.role: Accessible.List Accessible.name: mainTitle ScrollHelper { id: scrollHelper flickable: contentDirectoryView anchors.fill: contentDirectoryView } 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 isPartial: false mainText: model.display fileUrl: model.url secondaryText: if (gridView.delegateDisplaySecondaryText) {model.secondaryText} else {""} imageUrl: model.imageUrl ? model.imageUrl : '' imageFallbackUrl: defaultIcon databaseId: model.databaseId delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText entryType: model.dataType - onEnqueue: gridView.enqueue(databaseId, name) - onReplaceAndPlay: gridView.replaceAndPlay(databaseId, name) + onEnqueue: gridView.enqueue(model.fullData, model.display) + onReplaceAndPlay: gridView.replaceAndPlay(model.fullData, model.display) onOpen: gridView.open(model.display, model.secondaryText, (model && model.imageUrl && model.imageUrl.toString() !== "" ? model.imageUrl : defaultIcon), model.databaseId, model.dataType) onSelected: { forceActiveFocus() contentDirectoryView.currentIndex = model.index } onActiveFocusChanged: { if (activeFocus && contentDirectoryView.currentIndex !== model.index) { contentDirectoryView.currentIndex = model.index } } } } } } } diff --git a/src/qml/ListBrowserDelegate.qml b/src/qml/ListBrowserDelegate.qml index bd9207cf..981c23bb 100644 --- a/src/qml/ListBrowserDelegate.qml +++ b/src/qml/ListBrowserDelegate.qml @@ -1,405 +1,405 @@ /* SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien SPDX-FileCopyrightText: 2017 (c) Alexander Stippich SPDX-License-Identifier: LGPL-3.0-or-later */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.3 import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { id: mediaTrack property url trackUrl property var dataType property string title property string artist property string album property string albumArtist property string duration property url imageUrl property int trackNumber property int discNumber property int rating property bool hideDiscNumber property bool isSelected property bool isAlternateColor property bool detailedView: true signal clicked() - signal enqueue(var url, var entryType, var name) - signal replaceAndPlay(var url, var entryType, var name) + signal enqueue() + signal replaceAndPlay() signal callOpenMetaDataView(var url, var entryType) Accessible.role: Accessible.ListItem Accessible.name: title Accessible.description: title - Keys.onReturnPressed: enqueue(trackUrl, dataType, title) - Keys.onEnterPressed: enqueue(trackUrl, dataType, title) + Keys.onReturnPressed: enqueue() + Keys.onEnterPressed: enqueue() property int singleLineHeight: 3 * Kirigami.Units.smallSpacing + Kirigami.Units.gridUnit height: singleLineHeight + (detailedView ? Kirigami.Units.gridUnit : 0) Rectangle { id: rowRoot anchors.fill: parent z: 1 color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } MouseArea { id: hoverArea anchors.fill: parent z: 2 hoverEnabled: true acceptedButtons: Qt.LeftButton onClicked: { mediaTrack.clicked() } - onDoubleClicked: enqueue(trackUrl, dataType, title) + onDoubleClicked: enqueue() RowLayout { anchors.fill: parent spacing: 0 LabelWithToolTip { id: mainLabel visible: !detailedView text: { if (trackNumber !== 0 && trackNumber !== -1 && trackNumber !== undefined) { if (albumArtist !== undefined && artist !== albumArtist) return i18nc("%1: track number. %2: track title. %3: artist name", "%1 - %2 - %3", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title, artist); else return i18nc("%1: track number. %2: track title.", "%1 - %2", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title); } else { if (albumArtist !== undefined && artist !== albumArtist) return i18nc("%1: track title. %2: artist name", "%1 - %2", title, artist); else return i18nc("%1: track title", "%1", title); } } elide: Text.ElideRight horizontalAlignment: Text.AlignLeft color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.fillWidth: true Layout.leftMargin: { if (!LayoutMirroring.enabled) return (!hideDiscNumber ? Kirigami.Units.largeSpacing * 4 : Kirigami.Units.largeSpacing) else return 0 } Layout.rightMargin: { if (LayoutMirroring.enabled) return (!hideDiscNumber ? Kirigami.Units.largeSpacing * 4 : Kirigami.Units.largeSpacing) else return 0 } } ImageWithFallback { id: coverImageElement Layout.preferredHeight: mediaTrack.height - Kirigami.Units.smallSpacing Layout.preferredWidth: mediaTrack.height - Kirigami.Units.smallSpacing Layout.leftMargin: !LayoutMirroring.enabled ? Kirigami.Units.smallSpacing : 0 Layout.rightMargin: LayoutMirroring.enabled ? Kirigami.Units.smallSpacing : 0 Layout.alignment: Qt.AlignCenter visible: detailedView sourceSize.width: mediaTrack.height - Kirigami.Units.smallSpacing sourceSize.height: mediaTrack.height - Kirigami.Units.smallSpacing fillMode: Image.PreserveAspectFit smooth: true source: imageUrl fallback: elisaTheme.defaultAlbumImage asynchronous: true layer.enabled: !usingFallback layer.effect: DropShadow { source: coverImageElement radius: 10 spread: 0.1 samples: 21 color: myPalette.shadow } } ColumnLayout { visible: detailedView Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignLeft spacing: 0 LabelWithToolTip { id: mainLabelDetailed level: 4 text: { if (trackNumber >= 0) { return i18nc("%1: track number. %2: track title", "%1 - %2", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title); } else { return title; } } horizontalAlignment: Text.AlignLeft color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.leftMargin: !LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0 Layout.rightMargin: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0 Layout.fillWidth: true Layout.topMargin: Kirigami.Units.smallSpacing / 2 elide: Text.ElideRight } Item { Layout.fillHeight: true } LabelWithToolTip { id: artistLabel text: { var labelText = "" if (artist) { labelText += artist } if (album !== '') { labelText += ' - ' + album if (!hideDiscNumber && discNumber !== -1) { labelText += ' - CD ' + discNumber } } return labelText; } horizontalAlignment: Text.AlignLeft opacity: 0.6 color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignBottom Layout.leftMargin: !LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0 Layout.rightMargin: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0 Layout.fillWidth: true Layout.bottomMargin: Kirigami.Units.smallSpacing / 2 elide: Text.ElideRight } } Loader { id: hoverLoader active: false Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.rightMargin: 10 z: 1 opacity: 0 sourceComponent: Row { anchors.centerIn: parent FlatButtonWithToolTip { id: detailsButton height: singleLineHeight width: singleLineHeight text: i18nc("Show track metadata", "View Details") icon.name: "help-about" onClicked: callOpenMetaDataView(trackUrl, dataType) } FlatButtonWithToolTip { id: enqueueButton height: singleLineHeight width: singleLineHeight text: i18nc("Enqueue current track", "Enqueue") icon.name: "list-add" - onClicked: enqueue(trackUrl, dataType, title) + onClicked: enqueue() } FlatButtonWithToolTip { id: clearAndEnqueueButton scale: LayoutMirroring.enabled ? -1 : 1 height: singleLineHeight width: singleLineHeight text: i18nc("Clear play list and enqueue current track", "Play Now and Replace Play List") icon.name: "media-playback-start" - onClicked: replaceAndPlay(trackUrl, dataType, title) + onClicked: replaceAndPlay() } } } RatingStar { id: ratingWidget starRating: rating Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.leftMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing } LabelWithToolTip { id: durationLabel text: duration font.weight: Font.Light color: myPalette.text horizontalAlignment: Text.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.rightMargin: !LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0 Layout.leftMargin: LayoutMirroring.enabled ? Kirigami.Units.largeSpacing : 0 } } } states: [ State { name: 'notSelected' when: !mediaTrack.activeFocus && !hoverArea.containsMouse && !mediaTrack.isSelected PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 0.0 } PropertyChanges { target: rowRoot color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } PropertyChanges { target: rowRoot opacity: 1 } }, State { name: 'hovered' when: !mediaTrack.activeFocus && hoverArea.containsMouse PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } PropertyChanges { target: rowRoot color: myPalette.highlight } PropertyChanges { target: rowRoot opacity: 0.2 } }, State { name: 'selected' when: !mediaTrack.activeFocus && mediaTrack.isSelected PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } PropertyChanges { target: rowRoot color: myPalette.mid } PropertyChanges { target: rowRoot opacity: 1. } }, State { name: 'focused' when: mediaTrack.activeFocus PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } PropertyChanges { target: rowRoot color: myPalette.highlight } PropertyChanges { target: rowRoot opacity: 0.6 } } ] }