From b66f2602f75ea891f1178533c6ddb82c3c1ed44b Mon Sep 17 00:00:00 2001 From: Casper Jeukendrup <48658420+cbjeukendrup@users.noreply.github.com> Date: Thu, 7 May 2026 20:48:12 +0200 Subject: [PATCH] Move Home > Scores > Cloud scores components into project module Near all other Home > Scores components --- .../qml/MuseScore/Project/CMakeLists.txt | 10 +- .../qml/MuseScore/Project/ScoresPage.qml | 3 - .../ScoresPage/CloudScoresGridView.qml | 110 ++++++ .../ScoresPage/CloudScoresListView.qml | 327 ++++++++++++++++++ .../internal/ScoresPage/CloudScoresView.qml | 221 ++++++++++++ .../ScoresPage}/ScoresGridView.qml | 2 - .../ScoresPage}/ScoresListView.qml | 2 - .../{ => internal/ScoresPage}/ScoresView.qml | 0 8 files changed, 665 insertions(+), 10 deletions(-) create mode 100644 src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresGridView.qml create mode 100644 src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresListView.qml create mode 100644 src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml rename src/project/qml/MuseScore/Project/{ => internal/ScoresPage}/ScoresGridView.qml (99%) rename src/project/qml/MuseScore/Project/{ => internal/ScoresPage}/ScoresListView.qml (99%) rename src/project/qml/MuseScore/Project/{ => internal/ScoresPage}/ScoresView.qml (100%) diff --git a/src/project/qml/MuseScore/Project/CMakeLists.txt b/src/project/qml/MuseScore/Project/CMakeLists.txt index 3a9c37bd50da8..d0a84c5175eba 100644 --- a/src/project/qml/MuseScore/Project/CMakeLists.txt +++ b/src/project/qml/MuseScore/Project/CMakeLists.txt @@ -98,9 +98,15 @@ qt_add_qml_module(project_qml internal/Properties/PropertyItem.qml internal/SaveToCloud/SaveLocationOption.qml internal/ScoresPage/CloudScoreIndicatorButton.qml + internal/ScoresPage/CloudScoresGridView.qml + internal/ScoresPage/CloudScoresListView.qml + internal/ScoresPage/CloudScoresView.qml internal/ScoresPage/RecentScoresView.qml internal/ScoresPage/ScoreGridItem.qml internal/ScoresPage/ScoreListItem.qml + internal/ScoresPage/ScoresGridView.qml + internal/ScoresPage/ScoresListView.qml + internal/ScoresPage/ScoresView.qml internal/ScoresPage/ScoreThumbnail.qml Message.qml MigrationDialog.qml @@ -108,10 +114,7 @@ qt_add_qml_module(project_qml ProjectPropertiesDialog.qml ProjectUploadedDialog.qml SaveToCloudDialog.qml - ScoresGridView.qml - ScoresListView.qml ScoresPage.qml - ScoresView.qml UploadProgressDialog.qml RESOURCES internal/Migration/migration.png @@ -125,6 +128,7 @@ qt_add_qml_module(project_qml resources/AudioCom_Waveform.png resources/PublishScores.png IMPORTS + TARGET muse_cloud_qml TARGET muse_ui_qml TARGET muse_uicomponents_qml TARGET notationscene_qml diff --git a/src/project/qml/MuseScore/Project/ScoresPage.qml b/src/project/qml/MuseScore/Project/ScoresPage.qml index ab8177329399e..000a4d201d71c 100644 --- a/src/project/qml/MuseScore/Project/ScoresPage.qml +++ b/src/project/qml/MuseScore/Project/ScoresPage.qml @@ -25,9 +25,6 @@ import QtQuick.Layouts import Muse.Ui import Muse.UiComponents import MuseScore.Project -import Muse.Cloud - -import "internal/ScoresPage" FocusScope { id: root diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresGridView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresGridView.qml new file mode 100644 index 0000000000000..48fe49f877381 --- /dev/null +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresGridView.qml @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2023 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 . + */ +import QtQuick + +import Muse.Ui +import Muse.UiComponents +import MuseScore.Project + +ScoresGridView { + id: root + + navigation.name: "OnlineScoresGrid" + navigation.accessible.name: qsTrc("project", "Online scores grid") + + isNoResultsMessageAllowed: model.state === CloudScoresModel.Fine + + Component.onCompleted: { + prv.updateDesiredRowCount() + } + + Connections { + target: root.model + + function onStateChanged() { + if (root.model.state === CloudScoresModel.Fine) { + // After the model has loaded more, check if even more is needed + prv.updateDesiredRowCount(); + } + } + } + + QtObject { + id: prv + + readonly property int remainingFullRowsBelowViewport: + Math.floor(root.view.count / root.view.columns) - Math.ceil((root.view.contentY + root.view.height) / root.view.cellHeight) + + readonly property bool isSatisfied: remainingFullRowsBelowViewport >= 3 + + onIsSatisfiedChanged: { + if (!isSatisfied) { + updateDesiredRowCount(); + } + } + + property bool updateDesiredRowCountScheduled: false + + function updateDesiredRowCount() { + if (updateDesiredRowCountScheduled) { + return + } + + if (isSatisfied || !root.model.hasMore) { + return + } + + updateDesiredRowCountScheduled = true + + Qt.callLater(function() { + let newDesiredRowCount = root.model.rowCount + (3 - remainingFullRowsBelowViewport) * view.columns + + if (root.model.desiredRowCount < newDesiredRowCount) { + root.model.desiredRowCount = newDesiredRowCount + } + + updateDesiredRowCountScheduled = false + }) + } + } + + view.footer: root.model.state === CloudScoresModel.Loading + ? busyIndicatorComp : null + + Component { + id: busyIndicatorComp + + Item { + width: GridView.view ? GridView.view.width : 0 + height: indicator.implicitHeight + indicator.anchors.topMargin + indicator.anchors.bottomMargin + + StyledBusyIndicator { + id: indicator + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: root.view.spacingBetweenRows / 2 + anchors.bottomMargin: root.view.spacingBetweenRows / 2 + } + } + } +} \ No newline at end of file diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresListView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresListView.qml new file mode 100644 index 0000000000000..ffa0b3641f33a --- /dev/null +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresListView.qml @@ -0,0 +1,327 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2023 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 . + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Muse.Ui +import Muse.UiComponents +import MuseScore.Project +import Muse.Cloud + +ScoresListView { + id: root + + navigation.name: "OnlineScoresList" + navigation.accessible.name: qsTrc("project", "Online scores list") + + Component.onCompleted: { + prv.updateDesiredRowCount() + } + + Connections { + target: root.model + function onStateChanged() { + if (root.model.state === CloudScoresModel.Fine) { + // After the model has loaded more, check if even more is needed + prv.updateDesiredRowCount(); + } + } + } + + QtObject { + id: prv + + readonly property int remainingScoresBelowViewport: + root.view.count - Math.ceil((root.view.contentY + root.view.height) / root.view.rowHeight) + + readonly property bool isSatisfied: remainingScoresBelowViewport >= 20 + + onIsSatisfiedChanged: { + if (!isSatisfied) { + updateDesiredRowCount(); + } + } + + property bool updateDesiredRowCountScheduled: false + + function updateDesiredRowCount() { + if (updateDesiredRowCountScheduled) { + return + } + + if (isSatisfied || !root.model.hasMore) { + return + } + + updateDesiredRowCountScheduled = true + + Qt.callLater(function() { + let newDesiredRowCount = root.model.rowCount + (20 - remainingScoresBelowViewport) + + if (root.model.desiredRowCount < newDesiredRowCount) { + root.model.desiredRowCount = newDesiredRowCount + } + + updateDesiredRowCountScheduled = false + }) + } + } + + columns: [ + //! Note: sometimes, in the `delegate` item, some properties are used that don't seem to be defined: + //! `score`, `navigationPanel`, `navigationRow`, `navigationColumnStart`, `listItem` + //! These properties are provided by the `Loader` inside `columnsRepeater` in `ScoreListItem.qml`. + + ScoresListView.ColumnItem { + id: visibilityColumn + header: qsTrc("project/cloud", "Visibility") + + width: function(parentWidth) { + let parentWidthExclusingSpacing = parentWidth - root.columns.length * root.view.columnSpacing; + return 0.16 * parentWidthExclusingSpacing + } + + delegate: Item { + id: visibilityContainer + + implicitWidth: visibilityRow.implicitWidth + implicitHeight: visibilityRow.implicitHeight + + visible: !visibilityLabel.isEmpty + + readonly property var iconAndText: { + switch (score.cloudVisibility ?? 0) { + case CloudVisibility.Private: + return { "iconCode": IconCode.LOCK_CLOSED, "text": qsTrc("project/cloud", "Private") } + case CloudVisibility.Unlisted: + return { "iconCode": IconCode.LOCK_OPEN, "text": qsTrc("project/cloud", "Unlisted") } + case CloudVisibility.Public: + return { "iconCode": IconCode.GLOBE, "text": qsTrc("project/cloud", "Public") } + } + return { "iconCode": IconCode.NONE, "text": "" } + } + + NavigationFocusBorder { + navigationCtrl: NavigationControl { + name: "VisibilityLabel" + panel: navigationPanel + row: navigationRow + column: navigationColumnStart + enabled: visibilityContainer.visible && visibilityContainer.enabled && !visibilityLabel.isEmpty + accessible.name: visibilityColumn.header + ": " + visibilityContainer.iconAndText.text + accessible.role: MUAccessible.StaticText + + onActiveChanged: { + if (active) { + listItem.scrollIntoView() + } + } + } + + anchors.margins: -radius + radius: 2 + border.width + } + + RowLayout { + id: visibilityRow + spacing: 8 + + StyledIconLabel { + iconCode: visibilityContainer.iconAndText.iconCode + } + + StyledTextLabel { + id: visibilityLabel + Layout.fillWidth: true + + text: visibilityContainer.iconAndText.text + + font: ui.theme.largeBodyFont + horizontalAlignment: Text.AlignLeft + } + } + } + }, + + ScoresListView.ColumnItem { + id: modifiedColumn + + //: Stands for "Last time that this score was modified". + //: Used as the header of this column in the scores list. + header: qsTrc("project", "Modified") + + width: function(parentWidth) { + let parentWidthExclusingSpacing = parentWidth - root.columns.length * root.view.columnSpacing; + return 0.16 * parentWidthExclusingSpacing + } + + delegate: StyledTextLabel { + id: modifiedLabel + text: score.timeSinceModified ?? "" + + font.capitalization: Font.AllUppercase + horizontalAlignment: Text.AlignLeft + + NavigationFocusBorder { + navigationCtrl: NavigationControl { + name: "ModifiedLabel" + panel: navigationPanel + row: navigationRow + column: navigationColumnStart + enabled: modifiedLabel.visible && modifiedLabel.enabled && !modifiedLabel.isEmpty + accessible.name: modifiedColumn.header + ": " + modifiedLabel.text + accessible.role: MUAccessible.StaticText + + onActiveChanged: { + if (active) { + listItem.scrollIntoView() + } + } + } + + anchors.margins: -radius + radius: 2 + border.width + } + } + }, + + ScoresListView.ColumnItem { + id: sizeColumn + header: qsTrc("global", "Size", "file size") + + width: function(parentWidth) { + let parentWidthExclusingSpacing = parentWidth - root.columns.length * root.view.columnSpacing; + return 0.13 * parentWidthExclusingSpacing + } + + delegate: StyledTextLabel { + id: sizeLabel + text: Boolean(score.fileSize) ? score.fileSize : "-" + + font: ui.theme.largeBodyFont + horizontalAlignment: Text.AlignLeft + + NavigationFocusBorder { + navigationCtrl: NavigationControl { + name: "SizeLabel" + panel: navigationPanel + row: navigationRow + column: navigationColumnStart + enabled: sizeLabel.visible && sizeLabel.enabled && !sizeLabel.isEmpty + accessible.name: sizeColumn.header + ": " + (Boolean(score.fileSize) ? score.fileSize : qsTrc("global", "Unknown")) + accessible.role: MUAccessible.StaticText + + onActiveChanged: { + if (active) { + listItem.scrollIntoView() + } + } + } + + anchors.margins: -radius + radius: 2 + border.width + } + } + }, + + ScoresListView.ColumnItem { + id: viewsColumn + + //: Stands for "The number of times this score was viewed on MuseScore.com". + //: Used as the header of this column in the scores list. + header: qsTrc("project", "Views", "number of views") + + width: function(parentWidth) { + let parentWidthExclusingSpacing = parentWidth - root.columns.length * root.view.columnSpacing; + return Math.max(0.08 * parentWidthExclusingSpacing, 76) + } + + delegate: Item { + id: viewsContainer + + implicitWidth: viewsRow.implicitWidth + implicitHeight: viewsRow.implicitHeight + + visible: !viewsLabel.isEmpty + + NavigationFocusBorder { + navigationCtrl: NavigationControl { + name: "ViewsLabel" + panel: navigationPanel + row: navigationRow + column: navigationColumnStart + enabled: viewsContainer.visible && viewsContainer.enabled + accessible.name: viewsColumn.header + ": " + viewsLabel.text + accessible.role: MUAccessible.StaticText + + onActiveChanged: { + if (active) { + listItem.scrollIntoView() + } + } + } + + anchors.margins: -radius + radius: 2 + border.width + } + + RowLayout { + id: viewsRow + spacing: 8 + + StyledIconLabel { + iconCode: IconCode.EYE_OPEN + } + + StyledTextLabel { + id: viewsLabel + Layout.fillWidth: true + + text: score.cloudViewCount ?? "" + + font: ui.theme.largeBodyFont + horizontalAlignment: Text.AlignLeft + } + } + } + } + ] + + view.footer: root.model.state === CloudScoresModel.Loading + ? busyIndicatorComp : null + + Component { + id: busyIndicatorComp + + Item { + width: ListView.view ? ListView.view.width : 0 + height: root.view.rowHeight + + StyledBusyIndicator { + id: indicator + + anchors.centerIn: parent + } + } + } +} \ No newline at end of file diff --git a/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml new file mode 100644 index 0000000000000..7bb59874c10ae --- /dev/null +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/CloudScoresView.qml @@ -0,0 +1,221 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-Studio-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ + +import QtQuick + +import Muse.Ui +import Muse.UiComponents +import Muse.Cloud +import MuseScore.Project + +ScoresView { + id: root + + CloudScoresModel { + id: cloudScoresModel + } + + Component.onCompleted: { + cloudScoresModel.load() + } + + function refresh() { + cloudScoresModel.reload() + } + + sourceComponent: { + switch (cloudScoresModel.state) { + case CloudScoresModel.NotSignedIn: + return notSignedInComp + case CloudScoresModel.Error: + return errorComp + case CloudScoresModel.Fine: + case CloudScoresModel.Loading: + break; + } + + if (cloudScoresModel.rowCount == 0 && !cloudScoresModel.hasMore && cloudScoresModel.state != CloudScoresModel.Loading) { + return emptyComp + } + + return root.viewType === ScoresPageModel.List ? listComp : gridComp + } + + Component { + id: gridComp + + CloudScoresGridView { + anchors.fill: parent + + model: cloudScoresModel + searchText: root.searchText + + backgroundColor: root.backgroundColor + sideMargin: root.sideMargin + + navigation.section: root.navigationSection + navigation.order: root.navigationOrder + + onCreateNewScoreRequested: { + root.createNewScoreRequested() + } + + onOpenScoreRequested: function(scorePath, displayName) { + root.openScoreRequested(scorePath, displayName) + } + } + } + + Component { + id: listComp + + CloudScoresListView { + anchors.fill: parent + + model: cloudScoresModel + searchText: root.searchText + + backgroundColor: root.backgroundColor + sideMargin: root.sideMargin + + navigation.section: root.navigationSection + navigation.order: root.navigationOrder + + onCreateNewScoreRequested: { + root.createNewScoreRequested() + } + + onOpenScoreRequested: function(scorePath, displayName) { + root.openScoreRequested(scorePath, displayName) + } + } + } + + Component { + id: emptyComp + + Item { + anchors.fill: parent + + Message { + anchors.top: parent.top + anchors.topMargin: Math.max(parent.height / 3 - height / 2, 0) + anchors.left: parent.left + anchors.leftMargin: root.sideMargin + anchors.right: parent.right + anchors.rightMargin: root.sideMargin + + title: qsTrc("project", "You don’t have any online scores yet") + body: qsTrc("project", "Scores will appear here when you save a file to the cloud, or publish a score on MuseScore.com.").arg("https://musescore.com") + } + } + } + + Component { + id: notSignedInComp + + Item { + anchors.fill: parent + + Column { + anchors.top: parent.top + anchors.topMargin: Math.max(parent.height / 3 - height / 2, 0) + anchors.left: parent.left + anchors.leftMargin: root.sideMargin + anchors.right: parent.right + anchors.rightMargin: root.sideMargin + + spacing: 32 + + Message { + width: parent.width + + title: qsTrc("project", "You are not signed in") + body: qsTrc("project", "Log in or create a new account on MuseScore.com to view online scores.").arg("https://musescore.com") + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + width: implicitWidth + spacing: 12 + + MuseScoreComAuthorizationModel { + id: authorizationModel + } + + Component.onCompleted: { + authorizationModel.load() + } + + NavigationPanel { + id: navPanel + name: "SignInButtons" + section: root.navigationSection + order: root.navigationOrder + direction: NavigationPanel.Horizontal + accessible.name: qsTrc("cloud", "Sign in buttons") + } + + FlatButton { + navigation.panel: navPanel + navigation.order: 1 + + text: qsTrc("cloud", "Create account") + onClicked: { + authorizationModel.createAccount() + } + } + + FlatButton { + navigation.panel: navPanel + navigation.order: 2 + + text: qsTrc("cloud", "Sign in") + onClicked: { + authorizationModel.signIn() + } + } + } + } + } + } + + Component { + id: errorComp + + Item { + anchors.fill: parent + + Message { + anchors.top: parent.top + anchors.topMargin: Math.max(parent.height / 3 - height / 2, 0) + anchors.left: parent.left + anchors.leftMargin: root.sideMargin + anchors.right: parent.right + anchors.rightMargin: root.sideMargin + + title: qsTrc("project", "Unable to load online scores") + body: qsTrc("global", "Please check your internet connection or try again later.") + } + } + } +} diff --git a/src/project/qml/MuseScore/Project/ScoresGridView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml similarity index 99% rename from src/project/qml/MuseScore/Project/ScoresGridView.qml rename to src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml index df7ce92b9c67a..84c0dcbb1fffd 100644 --- a/src/project/qml/MuseScore/Project/ScoresGridView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresGridView.qml @@ -26,8 +26,6 @@ import Muse.Ui import Muse.UiComponents import MuseScore.Project -import "internal/ScoresPage" - Item { id: root diff --git a/src/project/qml/MuseScore/Project/ScoresListView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml similarity index 99% rename from src/project/qml/MuseScore/Project/ScoresListView.qml rename to src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml index 88e120280d957..17d847d000e02 100644 --- a/src/project/qml/MuseScore/Project/ScoresListView.qml +++ b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresListView.qml @@ -27,8 +27,6 @@ import Muse.Ui import Muse.UiComponents import MuseScore.Project -import "internal/ScoresPage" - Item { id: root diff --git a/src/project/qml/MuseScore/Project/ScoresView.qml b/src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresView.qml similarity index 100% rename from src/project/qml/MuseScore/Project/ScoresView.qml rename to src/project/qml/MuseScore/Project/internal/ScoresPage/ScoresView.qml