diff --git a/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml b/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml index 1819f27e91c98b9c9adfba25208c3826efe1f311..836cfc89dad0ba619cdb28e05cae6c54d8fbc112 100644 --- a/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml +++ b/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml @@ -37,7 +37,7 @@ PlasmaComponents3.Page { Layout.preferredWidth: Layout.minimumWidth * 1.5 Layout.preferredHeight: Layout.minimumHeight * 1.5 - readonly property int controlSize: units.iconSizes.large + readonly property int controlSize: units.iconSizes.medium property double position: mpris2Source.currentData.Position || 0 readonly property real rate: mpris2Source.currentData.Rate || 1 @@ -129,381 +129,376 @@ PlasmaComponents3.Page { } } - ColumnLayout { // Main Column Layout - id: mainCol + Item { // Album Art Background + Details anchors.fill: parent - Item { // Album Art Background + Details - Layout.fillWidth: true - Layout.fillHeight: true + Image { + id: backgroundImage - Image { - id: backgroundImage + source: root.albumArt + sourceSize.width: 512 /* + * Setting a sourceSize.width here + * prevents flickering when resizing the + * plasmoid on a desktop. + */ - source: root.albumArt - sourceSize.width: 512 /* - * Setting a sourceSize.width here - * prevents flickering when resizing the - * plasmoid on a desktop. - */ - - anchors.fill: parent - anchors.margins: -units.smallSpacing*2 - fillMode: Image.PreserveAspectCrop + anchors.fill: parent + fillMode: Image.PreserveAspectCrop - asynchronous: true - visible: !!root.track && status === Image.Ready && !softwareRendering + asynchronous: true + visible: !!root.track && status === Image.Ready && !softwareRendering - layer.enabled: !softwareRendering - layer.effect: HueSaturation { - cached: true + layer.enabled: !softwareRendering + layer.effect: HueSaturation { + cached: true - lightness: -0.5 - saturation: 0.9 + lightness: -0.5 + saturation: 0.9 - layer.enabled: true - layer.effect: GaussianBlur { - cached: true + layer.enabled: true + layer.effect: GaussianBlur { + cached: true - radius: 128 - deviation: 12 - samples: 63 + radius: 128 + deviation: 12 + samples: 63 - transparentBorder: false - } + transparentBorder: false } } - RowLayout { // Album Art + Details - id: albumRow + } + RowLayout { // Album Art + Details + id: albumRow - anchors { - fill: parent - leftMargin: units.largeSpacing - rightMargin: units.largeSpacing - } + anchors { + fill: parent + leftMargin: units.largeSpacing + rightMargin: units.largeSpacing + } - spacing: units.largeSpacing + spacing: units.largeSpacing - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: 50 + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 50 - Image { // Album Art - id: albumArt + Image { // Album Art + id: albumArt - anchors.fill: parent + anchors.fill: parent - visible: !!root.track && status === Image.Ready + visible: !!root.track && status === Image.Ready - asynchronous: true + asynchronous: true - horizontalAlignment: Image.AlignRight - verticalAlignment: Image.AlignVCenter - fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignRight + verticalAlignment: Image.AlignVCenter + fillMode: Image.PreserveAspectFit - source: root.albumArt - } + source: root.albumArt + } - PlasmaCore.IconItem { // Fallback - visible: !albumArt.visible - source: { - if (mpris2Source.currentData["Desktop Icon Name"]) - return mpris2Source.currentData["Desktop Icon Name"] - return "media-album-cover" - } + PlasmaCore.IconItem { // Fallback + visible: !albumArt.visible + source: { + if (mpris2Source.currentData["Desktop Icon Name"]) + return mpris2Source.currentData["Desktop Icon Name"] + return "media-album-cover" + } - anchors { - fill: parent - margins: units.largeSpacing*2 - } + anchors { + fill: parent + margins: units.largeSpacing*2 } } + } - ColumnLayout { // Details Column - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredWidth: 50 - Layout.alignment: !(albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"]) ? Qt.AlignHCenter : 0 + ColumnLayout { // Details Column + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 50 + Layout.alignment: !(albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"]) ? Qt.AlignHCenter : 0 - /* - * We use Kirigami.Heading instead of PlasmaExtras.Heading - * to prevent a binding loop caused by the PC2 Label component - * used by PlasmaExtras.Heading - */ - Kirigami.Heading { // Song Title - id: songTitle - level: 1 + /* + * We use Kirigami.Heading instead of PlasmaExtras.Heading + * to prevent a binding loop caused by the PC2 Label component + * used by PlasmaExtras.Heading + */ + Kirigami.Heading { // Song Title + id: songTitle + level: 1 - color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" + color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" - textFormat: Text.PlainText - wrapMode: Text.Wrap - fontSizeMode: Text.VerticalFit - elide: Text.ElideRight + textFormat: Text.PlainText + wrapMode: Text.Wrap + fontSizeMode: Text.VerticalFit + elide: Text.ElideRight - text: root.track || i18n("No media playing") + text: root.track || i18n("No media playing") - Layout.fillWidth: true - Layout.maximumHeight: units.gridUnit*5 - } - Kirigami.Heading { // Song Artist - id: songArtist - visible: root.track && root.artist - level: 2 + Layout.fillWidth: true + Layout.maximumHeight: units.gridUnit*5 + } + Kirigami.Heading { // Song Artist + id: songArtist + visible: root.track && root.artist + level: 2 - color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" + color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" - textFormat: Text.PlainText - wrapMode: Text.Wrap - fontSizeMode: Text.VerticalFit - elide: Text.ElideRight + textFormat: Text.PlainText + wrapMode: Text.Wrap + fontSizeMode: Text.VerticalFit + elide: Text.ElideRight - text: root.artist - Layout.fillWidth: true - Layout.maximumHeight: units.gridUnit*2 - } - Kirigami.Heading { // Song Album - color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" - - level: 3 - opacity: 0.6 - - textFormat: Text.PlainText - wrapMode: Text.Wrap - fontSizeMode: Text.VerticalFit - elide: Text.ElideRight - - visible: text.length !== 0 - text: { - var metadata = root.currentMetadata - if (!metadata) { - return "" - } - var xesamAlbum = metadata["xesam:album"] - if (xesamAlbum) { - return xesamAlbum - } + text: root.artist + Layout.fillWidth: true + Layout.maximumHeight: units.gridUnit*2 + } + Kirigami.Heading { // Song Album + color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" - // if we play a local file without title and artist, show its containing folder instead - if (metadata["xesam:title"] || root.artist) { - return "" - } + level: 3 + opacity: 0.6 - var xesamUrl = (metadata["xesam:url"] || "").toString() - if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" - return "" - } + textFormat: Text.PlainText + wrapMode: Text.Wrap + fontSizeMode: Text.VerticalFit + elide: Text.ElideRight - var urlParts = xesamUrl.split("/") - if (urlParts.length < 3) { - return "" - } + visible: text.length !== 0 + text: { + var metadata = root.currentMetadata + if (!metadata) { + return "" + } + var xesamAlbum = metadata["xesam:album"] + if (xesamAlbum) { + return xesamAlbum + } - var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename - if (lastFolderPath) { - return lastFolderPath - } + // if we play a local file without title and artist, show its containing folder instead + if (metadata["xesam:title"] || root.artist) { + return "" + } + var xesamUrl = (metadata["xesam:url"] || "").toString() + if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" return "" } - Layout.fillWidth: true - Layout.maximumHeight: units.gridUnit*2 - } - } - } - } - Item { - implicitHeight: units.smallSpacing - } + var urlParts = xesamUrl.split("/") + if (urlParts.length < 3) { + return "" + } - RowLayout { // Seek Bar - spacing: units.smallSpacing + var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename + if (lastFolderPath) { + return lastFolderPath + } - // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case - enabled: !root.noPlayer && root.track && expandedRepresentation.length > 0 ? true : false - opacity: enabled ? 1 : 0 - Behavior on opacity { - NumberAnimation { duration: units.longDuration } + return "" + } + Layout.fillWidth: true + Layout.maximumHeight: units.gridUnit*2 + } } + } + } - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.maximumWidth: Math.min(units.gridUnit*45, Math.round(expandedRepresentation.width*(7/10))) + footer: PlasmaExtras.PlasmoidHeading { + id: footerItem + location: PlasmaExtras.PlasmoidHeading.Location.Footer + ColumnLayout { // Main Column Layout + anchors.fill: parent + RowLayout { // Seek Bar + spacing: units.smallSpacing + + // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case + enabled: !root.noPlayer && root.track && expandedRepresentation.length > 0 ? true : false + opacity: enabled ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: units.longDuration } + } - // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song - TextMetrics { - id: timeMetrics - text: i18nc("Remaining time for song e.g -5:42", "-%1", - KCoreAddons.Format.formatDuration(seekSlider.to / 1000, expandedRepresentation.durationFormattingOptions)) - font: theme.smallestFont - } + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.maximumWidth: Math.min(units.gridUnit*45, Math.round(expandedRepresentation.width*(7/10))) + + // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song + TextMetrics { + id: timeMetrics + text: i18nc("Remaining time for song e.g -5:42", "-%1", + KCoreAddons.Format.formatDuration(seekSlider.to / 1000, expandedRepresentation.durationFormattingOptions)) + font: theme.smallestFont + } - PlasmaComponents3.Label { // Time Elapsed - Layout.preferredWidth: timeMetrics.width - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignRight - text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) - opacity: 0.9 - font: theme.smallestFont - color: PlasmaCore.ColorScope.textColor - } + PlasmaComponents3.Label { // Time Elapsed + Layout.preferredWidth: timeMetrics.width + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) + opacity: 0.9 + font: theme.smallestFont + color: PlasmaCore.ColorScope.textColor + } - PlasmaComponents3.Slider { // Slider - id: seekSlider - Layout.fillWidth: true - z: 999 - value: 0 - visible: canSeek - - onMoved: { - if (!disablePositionUpdate) { - // delay setting the position to avoid race conditions - queuedPositionUpdate.restart() + PlasmaComponents3.Slider { // Slider + id: seekSlider + Layout.fillWidth: true + z: 999 + value: 0 + visible: canSeek + + onMoved: { + if (!disablePositionUpdate) { + // delay setting the position to avoid race conditions + queuedPositionUpdate.restart() + } } - } - Timer { - id: seekTimer - interval: 1000 / expandedRepresentation.rate - repeat: true - running: root.state === "playing" && plasmoid.expanded && !keyPressed && interval > 0 && seekSlider.to >= 1000000 - onTriggered: { - // some players don't continuously update the seek slider position via mpris - // add one second; value in microseconds - if (!seekSlider.pressed) { - disablePositionUpdate = true - if (seekSlider.value == seekSlider.to) { - retrievePosition(); - } else { - seekSlider.value += 1000000 + Timer { + id: seekTimer + interval: 1000 / expandedRepresentation.rate + repeat: true + running: root.state === "playing" && plasmoid.expanded && !keyPressed && interval > 0 && seekSlider.to >= 1000000 + onTriggered: { + // some players don't continuously update the seek slider position via mpris + // add one second; value in microseconds + if (!seekSlider.pressed) { + disablePositionUpdate = true + if (seekSlider.value == seekSlider.to) { + retrievePosition(); + } else { + seekSlider.value += 1000000 + } + disablePositionUpdate = false } - disablePositionUpdate = false } } } - } - RowLayout { - visible: !canSeek + RowLayout { + visible: !canSeek - Layout.fillWidth: true - Layout.preferredHeight: seekSlider.height + Layout.fillWidth: true + Layout.preferredHeight: seekSlider.height - PlasmaComponents3.ProgressBar { // Time Remaining - value: seekSlider.value - from: seekSlider.from - to: seekSlider.to + PlasmaComponents3.ProgressBar { // Time Remaining + value: seekSlider.value + from: seekSlider.from + to: seekSlider.to - Layout.fillWidth: true - Layout.fillHeight: false - Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + Layout.fillHeight: false + Layout.alignment: Qt.AlignVCenter + } } - } - PlasmaComponents3.Label { - Layout.preferredWidth: timeMetrics.width - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignLeft - text: i18nc("Remaining time for song e.g -5:42", "-%1", - KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) - opacity: 0.9 - font: theme.smallestFont - color: PlasmaCore.ColorScope.textColor + PlasmaComponents3.Label { + Layout.preferredWidth: timeMetrics.width + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + text: i18nc("Remaining time for song e.g -5:42", "-%1", + KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) + opacity: 0.9 + font: theme.smallestFont + color: PlasmaCore.ColorScope.textColor + } } - } - RowLayout { // Player Controls - id: playerControls - - property bool enabled: root.canControl - property int controlsSize: theme.mSize(theme.defaultFont).height * 3 - - Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: PlasmaCore.Units.largeSpacing - spacing: units.smallSpacing - - PlasmaComponents3.ToolButton { // Previous - Layout.alignment: Qt.AlignVCenter - implicitWidth: expandedRepresentation.controlSize - implicitHeight: implicitWidth - enabled: playerControls.enabled && root.canGoPrevious - icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" - onClicked: { - seekSlider.value = 0 // Let the media start from beginning. Bug 362473 - root.action_previous() + RowLayout { // Player Controls + id: playerControls + + property bool enabled: root.canControl + property int controlsSize: theme.mSize(theme.defaultFont).height * 3 + + Layout.alignment: Qt.AlignHCenter + Layout.bottomMargin: PlasmaCore.Units.smallSpacing + spacing: units.smallSpacing + + PlasmaComponents3.ToolButton { // Previous + icon.width: expandedRepresentation.controlSize + icon.height: icon.width + Layout.alignment: Qt.AlignVCenter + enabled: playerControls.enabled && root.canGoPrevious + icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" + onClicked: { + seekSlider.value = 0 // Let the media start from beginning. Bug 362473 + root.action_previous() + } } - } - PlasmaComponents3.ToolButton { // Pause/Play - Layout.alignment: Qt.AlignVCenter - implicitWidth: Math.round(expandedRepresentation.controlSize * 1.5) - implicitHeight: implicitWidth - enabled: root.state == "playing" ? root.canPause : root.canPlay - icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start" - onClicked: root.togglePlaying() - } + PlasmaComponents3.ToolButton { // Pause/Play + icon.width: expandedRepresentation.controlSize + icon.height: icon.width + Layout.alignment: Qt.AlignVCenter + enabled: root.state == "playing" ? root.canPause : root.canPlay + icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start" + onClicked: root.togglePlaying() + } - PlasmaComponents3.ToolButton { // Next - Layout.alignment: Qt.AlignVCenter - implicitWidth: expandedRepresentation.controlSize - implicitHeight: implicitWidth - enabled: playerControls.enabled && root.canGoNext - icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" - onClicked: { - seekSlider.value = 0 // Let the media start from beginning. Bug 362473 - root.action_next() + PlasmaComponents3.ToolButton { // Next + icon.width: expandedRepresentation.controlSize + icon.height: icon.width + Layout.alignment: Qt.AlignVCenter + enabled: playerControls.enabled && root.canGoNext + icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" + onClicked: { + seekSlider.value = 0 // Let the media start from beginning. Bug 362473 + root.action_next() + } } } } } - footer: PlasmaExtras.PlasmoidHeading { - id: footerItem - location: PlasmaExtras.PlasmoidHeading.Location.Footer + header: PlasmaExtras.PlasmoidHeading { + id: headerItem + location: PlasmaExtras.PlasmoidHeading.Location.Header visible: playerList.model.length > 2 // more than one player, @multiplex is always there + //this removes top padding to allow tabbar to touch the edge + topPadding: topInset + bottomPadding: -bottomInset + implicitHeight: PlasmaCore.Units.gridUnit * 2 + PlasmaComponents3.TabBar { + id: playerSelector + position: PlasmaComponents3.TabBar.Header - RowLayout { anchors.fill: parent - PlasmaComponents3.TabBar { - id: playerSelector - position: PlasmaComponents3.TabBar.Footer - - Layout.fillWidth: true - //this removes top padding to allow tabbar to touch the edge - topPadding: -footerItem.topPadding - - implicitHeight: contentHeight + implicitHeight: contentHeight - Repeater { - id: playerList - model: root.mprisSourcesModel + Repeater { + id: playerList + model: root.mprisSourcesModel - delegate: PlasmaComponents3.TabButton { - icon.name: modelData["icon"] - icon.height: PlasmaCore.Units.iconSizes.smallMedium - Accessible.name: modelData["text"] - PlasmaComponents3.ToolTip { - text: modelData["text"] - } - // Keep the delegate centered by offsetting the padding removed in the parent - topPadding: verticalPadding + footerItem.topPadding - bottomPadding: verticalPadding - footerItem.topPadding - onClicked: { - disablePositionUpdate = true - mpris2Source.current = modelData["source"]; - disablePositionUpdate = false - } + delegate: PlasmaComponents3.TabButton { + anchors.top: parent.top + anchors.bottom: parent.bottom + icon.name: modelData["icon"] + icon.height: PlasmaCore.Units.iconSizes.smallMedium + Accessible.name: modelData["text"] + PlasmaComponents3.ToolTip { + text: modelData["text"] } - - onModelChanged: { - playerSelector.currentIndex = model.findIndex( - (data) => { return data.source === mpris2Source.current } - ) + // Keep the delegate centered by offsetting the padding removed in the parent + bottomPadding: verticalPadding + headerItem.bottomPadding + topPadding: verticalPadding - headerItem.bottomPadding + onClicked: { + disablePositionUpdate = true + mpris2Source.current = modelData["source"]; + disablePositionUpdate = false } } + + onModelChanged: { + playerSelector.currentIndex = model.findIndex( + (data) => { return data.source === mpris2Source.current } + ) + } } } }