diff --git a/framework/learn/qml/Muse/Learn/CMakeLists.txt b/framework/learn/qml/Muse/Learn/CMakeLists.txt
index b0ac4db7ad..dcf4491f11 100644
--- a/framework/learn/qml/Muse/Learn/CMakeLists.txt
+++ b/framework/learn/qml/Muse/Learn/CMakeLists.txt
@@ -26,6 +26,8 @@ qt_add_qml_module(muse_learn_qml
SOURCES
learnpagemodel.cpp
learnpagemodel.h
+ internal/playlistmodel.cpp
+ internal/playlistmodel.h
QML_FILES
LearnPage.qml
internal/ClassesPage.qml
diff --git a/framework/learn/qml/Muse/Learn/LearnPage.qml b/framework/learn/qml/Muse/Learn/LearnPage.qml
index cc7f63d78d..61e8c527d4 100644
--- a/framework/learn/qml/Muse/Learn/LearnPage.qml
+++ b/framework/learn/qml/Muse/Learn/LearnPage.qml
@@ -115,10 +115,6 @@ FocusScope {
navigation.order: 1
visible: tabBar.currentIndex !== 1 // Not visible for Classes tab
-
- onSearchTextChanged: {
- pageModel.setSearchText(searchText)
- }
}
}
@@ -205,7 +201,25 @@ FocusScope {
Playlist {
id: getStartedComp
- playlist: pageModel.startedPlaylist
+ playlist: SortFilterProxyModel {
+ sourceModel: pageModel.startedPlaylist
+ filters: [
+ FuzzyFilter {
+ id: fuzzyFilter
+ enabled: Boolean(fuzzyPattern)
+ fuzzyPattern: searchField.searchText
+ roleName: "searchKey"
+ caseSensitivity: Qt.CaseInsensitive
+ }
+ ]
+
+ sorters: [
+ FuzzyScoreSorter {
+ enabled: fuzzyFilter.enabled
+ fuzzyFilter: fuzzyFilter
+ }
+ ]
+ }
navigation.section: navSec
navigation.order: 3
diff --git a/framework/learn/qml/Muse/Learn/internal/Playlist.qml b/framework/learn/qml/Muse/Learn/internal/Playlist.qml
index 6cafb2e878..694bed2331 100644
--- a/framework/learn/qml/Muse/Learn/internal/Playlist.qml
+++ b/framework/learn/qml/Muse/Learn/internal/Playlist.qml
@@ -19,8 +19,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
+
+pragma ComponentBehavior: Bound
+
import QtQuick
-import QtQuick.Layouts
import QtQuick.Controls
import Muse.Ui
@@ -87,6 +89,11 @@ FocusScope {
}
delegate: Item {
+ id: cellWrap
+
+ required property var model
+ required property int index
+
height: view.cellHeight
width: view.cellWidth
@@ -98,21 +105,21 @@ FocusScope {
anchors.horizontalCenter: parent.horizontalCenter
navigation.panel: navPanel
- navigation.row: view.columns === 0 ? 0 : Math.floor(model.index / view.columns)
- navigation.column: model.index - (navigation.row * view.columns)
+ navigation.row: view.columns === 0 ? 0 : Math.floor(cellWrap.index / view.columns)
+ navigation.column: cellWrap.index - (navigation.row * view.columns)
navigation.onActiveChanged: {
if (navigation.active) {
- view.positionViewAtIndex(index, ListView.Contain)
+ view.positionViewAtIndex(cellWrap.index, ListView.Contain)
}
}
- title: modelData.title
- author: modelData.author
- duration: modelData.duration
- thumbnail: modelData.thumbnailUrl
+ title: cellWrap.model.title
+ author: cellWrap.model.author
+ duration: cellWrap.model.duration
+ thumbnail: cellWrap.model.thumbnailUrl
onClicked: {
- api.launcher.openUrl(modelData.url)
+ api.launcher.openUrl(cellWrap.model.url)
}
}
}
diff --git a/framework/learn/qml/Muse/Learn/internal/playlistmodel.cpp b/framework/learn/qml/Muse/Learn/internal/playlistmodel.cpp
new file mode 100644
index 0000000000..402f9eb9a0
--- /dev/null
+++ b/framework/learn/qml/Muse/Learn/internal/playlistmodel.cpp
@@ -0,0 +1,117 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "playlistmodel.h"
+
+#include
+#include
+
+#include "global/log.h"
+
+namespace muse::learn {
+PlaylistModel::PlaylistModel(QObject* parent)
+ : QAbstractListModel(parent)
+{
+}
+
+const Playlist& PlaylistModel::playlist()
+{
+ return m_playlist;
+}
+
+void PlaylistModel::setPlaylist(const Playlist& playlist)
+{
+ beginResetModel();
+ m_playlist = playlist;
+ endResetModel();
+}
+
+int PlaylistModel::rowCount(const QModelIndex& parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return m_playlist.size();
+}
+
+QVariant PlaylistModel::data(const QModelIndex& index, int roleId) const
+{
+ IF_ASSERT_FAILED(index.isValid() && index.row() < m_playlist.size()) {
+ return QVariant();
+ }
+
+ const PlaylistItem& item = m_playlist[index.row()];
+
+ switch (roleId) {
+ case RoleTitle:
+ return item.title;
+ case RoleAuthor:
+ return item.author;
+ case RoleDuration: {
+ // h:mm:ss for anything over an hour
+ // m:ss for anything under an hour
+ // 0:ss for anything under a minute
+ int seconds = item.durationSecs;
+ int minutes = seconds / 60;
+ seconds -= minutes * 60;
+ int hours = minutes / 60;
+ minutes -= hours * 60;
+
+ return ((hours > 0)
+ ? (QString::number(hours) + ":" + QString::number(minutes).rightJustified(2, '0'))
+ : QString::number(minutes)
+ ) + ":"
+ + QString::number(seconds).rightJustified(2, '0');
+ }
+ case RoleUrl:
+ return item.url;
+ case RoleThumbnailUrl:
+ return item.thumbnailUrl;
+ case RoleSearchKey: {
+ QStringList searchKeyItems;
+ searchKeyItems << item.title
+ << item.author;
+
+ return searchKeyItems.join(u' ');
+ }
+
+ default:
+ UNREACHABLE;
+ break;
+ }
+
+ return QVariant();
+}
+
+QHash PlaylistModel::roleNames() const
+{
+ return QHash {
+ { RoleTitle, "title" },
+ { RoleAuthor, "author" },
+ { RoleDuration, "duration" },
+ { RoleUrl, "url" },
+ { RoleThumbnailUrl, "thumbnailUrl" },
+ { RoleSearchKey, "searchKey" },
+ };
+}
+}
diff --git a/framework/learn/qml/Muse/Learn/internal/playlistmodel.h b/framework/learn/qml/Muse/Learn/internal/playlistmodel.h
new file mode 100644
index 0000000000..7faec24faa
--- /dev/null
+++ b/framework/learn/qml/Muse/Learn/internal/playlistmodel.h
@@ -0,0 +1,56 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "learn/learntypes.h"
+
+namespace muse::learn {
+class PlaylistModel final : public QAbstractListModel
+{
+ Q_OBJECT
+
+public:
+ explicit PlaylistModel(QObject* parent = nullptr);
+
+ const Playlist& playlist();
+ void setPlaylist(const Playlist&);
+
+ int rowCount(const QModelIndex& parent) const override;
+ QVariant data(const QModelIndex& index, int roleId) const override;
+ QHash roleNames() const override;
+
+private:
+ enum Roles {
+ RoleTitle = Qt::UserRole,
+ RoleAuthor,
+ RoleDuration,
+ RoleUrl,
+ RoleThumbnailUrl,
+ RoleSearchKey,
+ };
+
+ Playlist m_playlist;
+};
+}
diff --git a/framework/learn/qml/Muse/Learn/learnpagemodel.cpp b/framework/learn/qml/Muse/Learn/learnpagemodel.cpp
index 373c5f8d5e..0ff34896d7 100644
--- a/framework/learn/qml/Muse/Learn/learnpagemodel.cpp
+++ b/framework/learn/qml/Muse/Learn/learnpagemodel.cpp
@@ -19,59 +19,45 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-#include "learnpagemodel.h"
-#include
-#include
+#include "learnpagemodel.h"
#include "translation.h"
using namespace muse::learn;
LearnPageModel::LearnPageModel(QObject* parent)
- : QObject(parent), Contextable(muse::iocCtxForQmlObject(this))
+ : QObject(parent),
+ m_gettingStartedModel{new PlaylistModel(this)},
+ m_advancedModel{new PlaylistModel(this)}
{
}
-QVariantList LearnPageModel::startedPlaylist() const
+PlaylistModel* LearnPageModel::startedPlaylist() const
{
- Playlist filteredPlaylist = filterPlaylistBySearch(m_startedPlaylist);
- return playlistToVariantList(filteredPlaylist);
+ return m_gettingStartedModel;
}
-QVariantList LearnPageModel::advancedPlaylist() const
+PlaylistModel* LearnPageModel::advancedPlaylist() const
{
- Playlist filteredPlaylist = filterPlaylistBySearch(m_advancedPlaylist);
- return playlistToVariantList(filteredPlaylist);
+ return m_advancedModel;
}
void LearnPageModel::load()
{
learnService()->refreshPlaylists();
- setStartedPlaylist(learnService()->startedPlaylist());
- learnService()->startedPlaylistChanged().onReceive(this, [this](const Playlist& playlist) {
- setStartedPlaylist(playlist);
+ m_gettingStartedModel->setPlaylist(learnService()->startedPlaylist());
+ learnService()->startedPlaylistChanged().onReceive(this, [this] (const Playlist& playlist) {
+ m_gettingStartedModel->setPlaylist(playlist);
});
- setAdvancedPlaylist(learnService()->advancedPlaylist());
- learnService()->advancedPlaylistChanged().onReceive(this, [this](const Playlist& playlist) {
- setAdvancedPlaylist(playlist);
+ m_advancedModel->setPlaylist(learnService()->advancedPlaylist());
+ learnService()->advancedPlaylistChanged().onReceive(this, [this] (const Playlist& playlist) {
+ m_advancedModel->setPlaylist(playlist);
});
}
-void LearnPageModel::setSearchText(const QString& text)
-{
- if (m_searchText == text) {
- return;
- }
-
- m_searchText = text;
-
- emit startedPlaylistChanged();
- emit advancedPlaylistChanged();
-}
-
QVariantMap LearnPageModel::classesAuthor() const
{
QVariantMap author;
@@ -104,72 +90,3 @@ bool LearnPageModel::classesEnabled()
{
return learnConfiguration()->classesEnabled();
}
-
-void LearnPageModel::setStartedPlaylist(Playlist startedPlaylist)
-{
- if (m_startedPlaylist == startedPlaylist) {
- return;
- }
-
- m_startedPlaylist = startedPlaylist;
- emit startedPlaylistChanged();
-}
-
-void LearnPageModel::setAdvancedPlaylist(Playlist advancedPlaylist)
-{
- if (m_advancedPlaylist == advancedPlaylist) {
- return;
- }
-
- m_advancedPlaylist = advancedPlaylist;
- emit advancedPlaylistChanged();
-}
-
-QVariantList LearnPageModel::playlistToVariantList(const Playlist& playlist) const
-{
- QVariantList result;
-
- // h:mm:ss for anything over an hour
- // m:ss for anything under an hour
- // 0:ss for anything under a minute
- auto formatDuration = [](int durationSecs) {
- int seconds = durationSecs;
- int minutes = seconds / 60;
- seconds -= minutes * 60;
- int hours = minutes / 60;
- minutes -= hours * 60;
-
- return ((hours > 0)
- ? (QString::number(hours) + ":" + QString::number(minutes).rightJustified(2, '0'))
- : QString::number(minutes)
- ) + ":"
- + QString::number(seconds).rightJustified(2, '0');
- };
-
- for (const PlaylistItem& item : playlist) {
- QVariantMap itemObj;
- itemObj["title"] = item.title;
- itemObj["author"] = item.author;
- itemObj["url"] = item.url;
- itemObj["thumbnailUrl"] = item.thumbnailUrl;
- itemObj["duration"] = formatDuration(item.durationSecs);
-
- result << itemObj;
- }
-
- return result;
-}
-
-Playlist LearnPageModel::filterPlaylistBySearch(const Playlist& playlist) const
-{
- Playlist result;
-
- for (const PlaylistItem& playlistItem : playlist) {
- if (playlistItem.title.contains(m_searchText, Qt::CaseInsensitive)
- || playlistItem.author.contains(m_searchText, Qt::CaseInsensitive)) {
- result.push_back(playlistItem);
- }
- }
-
- return result;
-}
diff --git a/framework/learn/qml/Muse/Learn/learnpagemodel.h b/framework/learn/qml/Muse/Learn/learnpagemodel.h
index 4d03541494..5af13d6b3a 100644
--- a/framework/learn/qml/Muse/Learn/learnpagemodel.h
+++ b/framework/learn/qml/Muse/Learn/learnpagemodel.h
@@ -27,20 +27,22 @@
#include
#include
-#include "async/asyncable.h"
+#include "global/async/asyncable.h"
+#include "global/modularity/ioc.h"
-#include "modularity/ioc.h"
-#include "ilearnservice.h"
-#include "ilearnconfiguration.h"
+#include "learn/ilearnconfiguration.h"
+#include "learn/ilearnservice.h"
+
+#include "internal/playlistmodel.h"
namespace muse::learn {
-class LearnPageModel : public QObject, public Contextable, public async::Asyncable
+class LearnPageModel : public QObject, public async::Asyncable
{
Q_OBJECT
QML_ELEMENT;
- Q_PROPERTY(QVariantList startedPlaylist READ startedPlaylist NOTIFY startedPlaylistChanged)
- Q_PROPERTY(QVariantList advancedPlaylist READ advancedPlaylist NOTIFY advancedPlaylistChanged)
+ Q_PROPERTY(QAbstractItemModel * startedPlaylist READ startedPlaylist CONSTANT)
+ Q_PROPERTY(QAbstractItemModel * advancedPlaylist READ advancedPlaylist CONSTANT)
GlobalInject learnConfiguration;
GlobalInject learnService;
@@ -48,30 +50,15 @@ class LearnPageModel : public QObject, public Contextable, public async::Asyncab
public:
explicit LearnPageModel(QObject* parent = nullptr);
- QVariantList startedPlaylist() const;
- QVariantList advancedPlaylist() const;
+ PlaylistModel* startedPlaylist() const;
+ PlaylistModel* advancedPlaylist() const;
Q_INVOKABLE void load();
- Q_INVOKABLE void setSearchText(const QString& text);
Q_INVOKABLE QVariantMap classesAuthor() const;
Q_INVOKABLE bool classesEnabled();
-private slots:
- void setStartedPlaylist(Playlist startedPlaylist);
- void setAdvancedPlaylist(Playlist advancedPlaylist);
-
-signals:
- void startedPlaylistChanged();
- void advancedPlaylistChanged();
-
private:
- QVariantList playlistToVariantList(const Playlist& playlist) const;
-
- Playlist filterPlaylistBySearch(const Playlist& playlist) const;
-
- Playlist m_startedPlaylist;
- Playlist m_advancedPlaylist;
-
- QString m_searchText;
+ PlaylistModel* m_gettingStartedModel = nullptr;
+ PlaylistModel* m_advancedModel = nullptr;
};
}