From 6d8f2375478cf130bdd8383c2e7f5c06106c85c2 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Sat, 30 May 2026 12:43:00 +0000 Subject: [PATCH] refactor(ui): extract label content component --- package/contents/ui/LabelContent.qml | 281 +++++++++++++++++++++++++ package/contents/ui/Representation.qml | 117 +--------- package/contents/ui/ShadowedLabel.qml | 42 ++++ 3 files changed, 334 insertions(+), 106 deletions(-) create mode 100644 package/contents/ui/LabelContent.qml create mode 100644 package/contents/ui/ShadowedLabel.qml diff --git a/package/contents/ui/LabelContent.qml b/package/contents/ui/LabelContent.qml new file mode 100644 index 0000000..430da07 --- /dev/null +++ b/package/contents/ui/LabelContent.qml @@ -0,0 +1,281 @@ +import Qt5Compat.GraphicalEffects +import QtQuick + +Item { + id: labelContent + + required property string artworkSource + required property var labelLines + required property string fontFamily + required property int fontPixelSize + required property string textColor + required property bool textShadowEnabled + required property int verticalSpacing + required property int horizontalAlignment + required property int borderRadius + required property int artworkSize + property string currentArtworkSource: "" + property string incomingArtworkSource: "" + property real currentArtworkOpacity: 0 + property real incomingArtworkOpacity: 0 + property bool crossfadeActive: false + readonly property bool artworkVisible: currentArtworkOpacity > 0 || incomingArtworkOpacity > 0 + readonly property bool holdsArtworkSlot: currentArtworkSource.length > 0 || artworkVisible + readonly property real textOpacity: crossfadeActive || (currentArtworkSource.length > 0 && incomingArtworkSource.length > 0) ? 0 : 1 - Math.max(currentArtworkOpacity, incomingArtworkOpacity) + + function resetIncomingArtwork() { + incomingArtworkSource = ""; + incomingArtworkOpacity = 0; + crossfadeActive = false; + } + + function finishIncomingArtwork() { + currentArtworkSource = incomingArtworkSource; + currentArtworkOpacity = incomingArtworkOpacity; + resetIncomingArtwork(); + } + + function clearCurrentArtwork() { + currentArtworkSource = ""; + currentArtworkOpacity = 0; + resetIncomingArtwork(); + } + + function startFadeOutToText() { + crossfadeAnimation.stop(); + fadeInIncomingArtwork.stop(); + resetIncomingArtwork(); + if (currentArtworkSource.length === 0 && currentArtworkOpacity === 0) { + clearCurrentArtwork(); + return ; + } + fadeOutCurrentArtwork.restart(); + } + + function handleArtworkSourceChange() { + const nextSource = artworkSource; + if (nextSource.length === 0) { + startFadeOutToText(); + return ; + } + if (nextSource === currentArtworkSource) { + fadeOutCurrentArtwork.stop(); + currentArtworkOpacity = currentArtworkSource.length > 0 ? 1 : 0; + resetIncomingArtwork(); + return ; + } + if (nextSource === incomingArtworkSource) + return ; + + fadeOutCurrentArtwork.stop(); + if (currentArtworkSource.length > 0) + currentArtworkOpacity = 1; + + crossfadeAnimation.stop(); + fadeInIncomingArtwork.stop(); + incomingArtworkSource = nextSource; + incomingArtworkOpacity = 0; + crossfadeActive = false; + } + + function startIncomingTransition() { + if (incomingArtworkSource.length === 0 || incomingArtworkSource !== artworkSource) + return ; + + fadeOutCurrentArtwork.stop(); + incomingArtworkOpacity = 0; + if (currentArtworkSource.length > 0 && currentArtworkOpacity > 0) { + crossfadeActive = true; + currentArtworkOpacity = 1; + crossfadeAnimation.restart(); + } else { + crossfadeActive = false; + currentArtworkOpacity = 0; + fadeInIncomingArtwork.restart(); + } + } + + function handleIncomingArtworkFailure() { + if (incomingArtworkSource.length === 0) + return ; + + resetIncomingArtwork(); + startFadeOutToText(); + } + + clip: holdsArtworkSlot + implicitHeight: holdsArtworkSlot ? artworkSize : labelTextColumn.implicitHeight + onArtworkSourceChanged: handleArtworkSourceChange() + Component.onCompleted: handleArtworkSourceChange() + + Image { + id: currentArtworkImage + + anchors.fill: parent + asynchronous: true + cache: true + fillMode: Image.PreserveAspectCrop + visible: false + source: labelContent.currentArtworkSource + sourceSize.width: labelContent.artworkSize + sourceSize.height: labelContent.artworkSize + } + + Image { + id: incomingArtworkImage + + anchors.fill: parent + asynchronous: true + cache: true + fillMode: Image.PreserveAspectCrop + visible: false + source: labelContent.incomingArtworkSource + sourceSize.width: labelContent.artworkSize + sourceSize.height: labelContent.artworkSize + onSourceChanged: labelContent.incomingArtworkOpacity = 0 + onStatusChanged: { + if (source.toString().length === 0) + return ; + + if (status === Image.Ready) + labelContent.startIncomingTransition(); + else if (status === Image.Error) + labelContent.handleIncomingArtworkFailure(); + } + } + + OpacityMask { + id: currentArtworkMask + + anchors.fill: parent + cached: true + source: currentArtworkImage + opacity: labelContent.currentArtworkOpacity + visible: labelContent.currentArtworkSource.length > 0 && opacity > 0 + + maskSource: Rectangle { + width: labelContent.width + height: labelContent.height + radius: labelContent.borderRadius + } + + } + + OpacityMask { + id: incomingArtworkMask + + anchors.fill: parent + cached: true + source: incomingArtworkImage + opacity: labelContent.incomingArtworkOpacity + visible: labelContent.incomingArtworkSource.length > 0 && opacity > 0 + + maskSource: Rectangle { + width: labelContent.width + height: labelContent.height + radius: labelContent.borderRadius + } + + } + + Column { + id: labelTextColumn + + anchors.left: parent.left + anchors.right: parent.right + spacing: labelContent.verticalSpacing + opacity: labelContent.textOpacity + visible: opacity > 0 + + Repeater { + model: labelContent.labelLines + + delegate: ShadowedLabel { + required property string modelData + + width: labelTextColumn.width + shadowEnabled: labelContent.textShadowEnabled + text: modelData + lineHeight: 0.8 + font.bold: true + font.pixelSize: labelContent.fontPixelSize + font.family: labelContent.fontFamily + color: labelContent.textColor + horizontalAlignment: labelContent.horizontalAlignment + } + + } + + Behavior on opacity { + NumberAnimation { + duration: 250 + easing.type: Easing.InOutQuad + } + + } + + } + + SequentialAnimation { + id: crossfadeAnimation + + ParallelAnimation { + NumberAnimation { + target: labelContent + property: "currentArtworkOpacity" + to: 0 + duration: 250 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: labelContent + property: "incomingArtworkOpacity" + to: 1 + duration: 250 + easing.type: Easing.InOutQuad + } + + } + + ScriptAction { + script: labelContent.finishIncomingArtwork() + } + + } + + SequentialAnimation { + id: fadeInIncomingArtwork + + NumberAnimation { + target: labelContent + property: "incomingArtworkOpacity" + to: 1 + duration: 250 + easing.type: Easing.InOutQuad + } + + ScriptAction { + script: labelContent.finishIncomingArtwork() + } + + } + + SequentialAnimation { + id: fadeOutCurrentArtwork + + NumberAnimation { + target: labelContent + property: "currentArtworkOpacity" + to: 0 + duration: 250 + easing.type: Easing.InOutQuad + } + + ScriptAction { + script: labelContent.clearCurrentArtwork() + } + + } + +} diff --git a/package/contents/ui/Representation.qml b/package/contents/ui/Representation.qml index 6e33a8d..7a6f7a8 100644 --- a/package/contents/ui/Representation.qml +++ b/package/contents/ui/Representation.qml @@ -1,9 +1,7 @@ -import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts import org.kde.kirigami as Kirigami -import org.kde.plasma.components as PlasmaComponents MouseArea { id: mediaControlsMouseArea @@ -166,72 +164,19 @@ MouseArea { Layout.fillWidth: true visible: mediaControlsMouseArea.nowPlayingLabelsVisible - Item { - id: labelContent - - readonly property bool artworkReady: mediaControlsMouseArea.configuredUseLabelArtwork && artworkImage.status === Image.Ready && artworkImage.source.toString().length > 0 - + LabelContent { Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight Layout.fillWidth: true - clip: artworkReady - implicitHeight: artworkReady ? mediaControlsMouseArea.labelRailWidth : labelTextColumn.implicitHeight - - Image { - id: artworkImage - - anchors.fill: parent - asynchronous: true - cache: true - fillMode: Image.PreserveAspectCrop - visible: false - source: mediaControlsMouseArea.configuredUseLabelArtwork ? player.artUrl : "" - sourceSize.width: mediaControlsMouseArea.labelRailWidth - sourceSize.height: mediaControlsMouseArea.labelRailWidth - } - - OpacityMask { - anchors.fill: parent - cached: true - source: artworkImage - visible: labelContent.artworkReady - - maskSource: Rectangle { - width: labelContent.width - height: labelContent.height - radius: mediaControlsMouseArea.effectiveImageBorderRadius - } - - } - - Column { - id: labelTextColumn - - anchors.left: parent.left - anchors.right: parent.right - spacing: mediaControlsMouseArea.effectiveLabelVerticalSpacing - visible: !labelContent.artworkReady - - Repeater { - model: mediaControlsMouseArea.effectiveLabelLines - - delegate: ShadowedLabel { - required property string modelData - - width: labelTextColumn.width - shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled - text: modelData - lineHeight: 0.8 - font.bold: true - font.pixelSize: mediaControlsMouseArea.effectiveLabelFontSize - font.family: mediaControlsMouseArea.configuredFontFamily - color: mediaControlsMouseArea.effectiveForegroundColor - horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignLeft : Text.AlignRight - } - - } - - } - + artworkSource: mediaControlsMouseArea.configuredUseLabelArtwork ? player.artUrl : "" + labelLines: mediaControlsMouseArea.effectiveLabelLines + fontFamily: mediaControlsMouseArea.configuredFontFamily + fontPixelSize: mediaControlsMouseArea.effectiveLabelFontSize + textColor: mediaControlsMouseArea.effectiveForegroundColor + textShadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled + verticalSpacing: mediaControlsMouseArea.effectiveLabelVerticalSpacing + horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignLeft : Text.AlignRight + borderRadius: mediaControlsMouseArea.effectiveImageBorderRadius + artworkSize: mediaControlsMouseArea.labelRailWidth } } @@ -380,44 +325,4 @@ MouseArea { } - component ShadowedLabel: Item { - id: shadowedLabel - - property alias text: foregroundLabel.text - property alias font: foregroundLabel.font - property alias lineHeight: foregroundLabel.lineHeight - property alias elide: foregroundLabel.elide - property alias horizontalAlignment: foregroundLabel.horizontalAlignment - property alias verticalAlignment: foregroundLabel.verticalAlignment - property alias color: foregroundLabel.color - property bool shadowEnabled: false - - implicitWidth: foregroundLabel.implicitWidth + (shadowEnabled ? 1 : 0) - implicitHeight: foregroundLabel.implicitHeight + (shadowEnabled ? 1 : 0) - - PlasmaComponents.Label { - id: shadowLabel - - visible: shadowedLabel.shadowEnabled - x: 1 - y: 1 - width: foregroundLabel.width - height: foregroundLabel.height - text: foregroundLabel.text - font: foregroundLabel.font - lineHeight: foregroundLabel.lineHeight - elide: foregroundLabel.elide - horizontalAlignment: foregroundLabel.horizontalAlignment - verticalAlignment: foregroundLabel.verticalAlignment - color: "#99000000" - } - - PlasmaComponents.Label { - id: foregroundLabel - - anchors.fill: parent - } - - } - } diff --git a/package/contents/ui/ShadowedLabel.qml b/package/contents/ui/ShadowedLabel.qml new file mode 100644 index 0000000..083bb0e --- /dev/null +++ b/package/contents/ui/ShadowedLabel.qml @@ -0,0 +1,42 @@ +import QtQuick +import org.kde.plasma.components as PlasmaComponents + +Item { + id: shadowedLabel + + property alias text: foregroundLabel.text + property alias font: foregroundLabel.font + property alias lineHeight: foregroundLabel.lineHeight + property alias elide: foregroundLabel.elide + property alias horizontalAlignment: foregroundLabel.horizontalAlignment + property alias verticalAlignment: foregroundLabel.verticalAlignment + property alias color: foregroundLabel.color + property bool shadowEnabled: false + + implicitWidth: foregroundLabel.implicitWidth + (shadowEnabled ? 1 : 0) + implicitHeight: foregroundLabel.implicitHeight + (shadowEnabled ? 1 : 0) + + PlasmaComponents.Label { + id: shadowLabel + + visible: shadowedLabel.shadowEnabled + x: 1 + y: 1 + width: foregroundLabel.width + height: foregroundLabel.height + text: foregroundLabel.text + font: foregroundLabel.font + lineHeight: foregroundLabel.lineHeight + elide: foregroundLabel.elide + horizontalAlignment: foregroundLabel.horizontalAlignment + verticalAlignment: foregroundLabel.verticalAlignment + color: "#99000000" + } + + PlasmaComponents.Label { + id: foregroundLabel + + anchors.fill: parent + } + +}