17 Commits

Author SHA1 Message Date
ruinivist 5a48b1b3a9 fix(ui): soften text shadow rendering 2026-05-30 14:48:51 +00:00
ruinivist 42268d7266 refactor(qml): replace qt5compat opacitymask with multieffect 2026-05-30 13:03:34 +00:00
ruinivist 6d8f237547 refactor(ui): extract label content component 2026-05-30 12:43:00 +00:00
ruinivist 45906f07c6 fix(ui): preserve separator spacing when hidden 2026-05-30 12:02:13 +00:00
ruinivist c26793dfe2 feat(ui): add hide separator setting 2026-05-30 11:59:43 +00:00
ruinivist de22c05c33 docs: add commit style instructions for agents 2026-05-30 11:53:34 +00:00
ruinivist 3d334b9896 feat: add configurable image border radius 2026-05-30 11:47:17 +00:00
ruinivist 7045ecf8e6 feat: add option to disable media controls 2026-05-30 11:14:45 +00:00
ruinivist b88c2ba3c5 feat: add optional label artwork mode 2026-05-30 11:08:56 +00:00
ruinivist d62c81762b fix: restore vertical centering for label 2026-05-23 14:18:15 +00:00
ruinivist 6102407288 feat: group config settings into sections 2026-05-23 14:12:55 +00:00
ruinivist 01ddf659d7 fix: stabilize hover controls 2026-05-23 13:52:26 +00:00
ruinivist 4551a63c32 feat: add configurable label and text sizing controls 2026-05-23 13:18:56 +00:00
ruinivist 023b869e9c feat: add configurable now playing label text 2026-05-23 12:35:58 +00:00
ruinivist a0a675bc99 feat: add configurable text and separator spacing 2026-05-23 12:23:38 +00:00
ruinivist 0ebd29ea02 docs: update readme 2026-04-15 20:19:05 +00:00
ruinivist 0798dd3ea2 feat: add configurable widget appearance settings 2026-04-15 20:13:10 +00:00
8 changed files with 1041 additions and 41 deletions
+19
View File
@@ -0,0 +1,19 @@
# AGENTS
## Commit Style
- Use Conventional Commits for every commit message.
- Required format: `type(scope): subject` or `type: subject`.
- Keep `type`, optional `scope`, and subject text lowercase.
- Use imperative, concise subjects.
- Proper nouns may keep their normal capitalization only when necessary.
Valid examples:
- `feat(ui): add artist text truncation`
- `fix: handle empty metadata values`
- `chore(release): bump plasmoid version`
Invalid examples:
- `Feat(ui): add artist text truncation` (uppercase type)
- `fix(UI): handle empty metadata values` (uppercase scope)
- `fix: Handle empty metadata values` (uppercase subject)
+14
View File
@@ -4,6 +4,8 @@ NowPlaying is a KDE Plasma 6 widget inspired by the Cleartext Rainmeter widget.
Now on KDE 6: https://www.pling.com/p/2195583/
> Too little too late but I plan to update it regularly now if any issues are created - ruinivist
## Prerequisites
You need the basic KDE and Qt 6 tools for Plasma widget development.
@@ -90,6 +92,18 @@ Enable the repository pre-commit hook:
make hooks-install
```
## Customization
The widget appearance can be tuned from the Plasma widget settings, including:
- custom multiline label text
- independent label, title, and artist font sizes
- title/artist vertical spacing
- NOW/PLAYING label vertical spacing
- separator gap from labels
- separator gap from track text
- separator height percentage
## Release Upload
Required GitHub repository secrets:
+81
View File
@@ -13,5 +13,86 @@
<label>Font family</label>
<default>Noto Sans</default>
</entry>
<entry name="labelFontSize" type="Int">
<label>Now Playing label font size</label>
<default>16</default>
</entry>
<entry name="titleFontSize" type="Int">
<label>Track title font size</label>
<default>28</default>
</entry>
<entry name="artistFontSize" type="Int">
<label>Track artist font size</label>
<default>26</default>
</entry>
<entry name="labelVisibilityMode" type="String">
<label>Now Playing label visibility mode</label>
<default>auto</default>
</entry>
<entry name="labelPlacement" type="String">
<label>Now Playing label placement</label>
<default>left</default>
</entry>
<entry name="labelText" type="String">
<label>Now Playing label text</label>
<default><![CDATA[NOW
PLAYING]]></default>
</entry>
<entry name="useLabelArtwork" type="Bool">
<label>Show track image when available</label>
<default>false</default>
</entry>
<entry name="imageBorderRadius" type="Int">
<label>Image border radius</label>
<default>8</default>
</entry>
<entry name="backgroundStyle" type="String">
<label>Background style</label>
<default>custom</default>
</entry>
<entry name="backgroundColor" type="String">
<label>Background color</label>
<default>transparent</default>
</entry>
<entry name="backgroundRadius" type="Int">
<label>Background radius</label>
<default>0</default>
</entry>
<entry name="foregroundColor" type="String">
<label>Foreground color</label>
<default>white</default>
</entry>
<entry name="textShadowEnabled" type="Bool">
<label>Enable text shadow</label>
<default>false</default>
</entry>
<entry name="showMediaControls" type="Bool">
<label>Show media controls</label>
<default>true</default>
</entry>
<entry name="trackTextVerticalSpacing" type="Int">
<label>Track text vertical spacing</label>
<default>5</default>
</entry>
<entry name="labelVerticalSpacing" type="Int">
<label>Now Playing label vertical spacing</label>
<default>0</default>
</entry>
<entry name="separatorGapLabel" type="Int">
<label>Separator gap from label rail</label>
<default>4</default>
</entry>
<entry name="separatorGapTrack" type="Int">
<label>Separator gap from track text</label>
<default>4</default>
</entry>
<entry name="separatorHeight" type="Int">
<label>Separator height percent</label>
<default>90</default>
</entry>
<entry name="hideSeparator" type="Bool">
<label>Hide separator</label>
<default>false</default>
</entry>
</group>
</kcfg>
+282
View File
@@ -0,0 +1,282 @@
import QtQuick
import QtQuick.Effects
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
opacity: 0
source: labelContent.currentArtworkSource
sourceSize.width: labelContent.artworkSize
sourceSize.height: labelContent.artworkSize
}
Image {
id: incomingArtworkImage
anchors.fill: parent
asynchronous: true
cache: true
fillMode: Image.PreserveAspectCrop
opacity: 0
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();
}
}
Rectangle {
id: artworkMaskShape
anchors.fill: parent
antialiasing: true
color: "white"
layer.enabled: true
radius: labelContent.borderRadius
visible: false
}
MultiEffect {
id: currentArtworkMask
anchors.fill: parent
autoPaddingEnabled: false
maskEnabled: true
maskSource: artworkMaskShape
source: currentArtworkImage
opacity: labelContent.currentArtworkOpacity
visible: labelContent.currentArtworkSource.length > 0 && opacity > 0
}
MultiEffect {
id: incomingArtworkMask
anchors.fill: parent
autoPaddingEnabled: false
maskEnabled: true
maskSource: artworkMaskShape
source: incomingArtworkImage
opacity: labelContent.incomingArtworkOpacity
visible: labelContent.incomingArtworkSource.length > 0 && opacity > 0
}
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()
}
}
}
+156 -39
View File
@@ -2,16 +2,88 @@ 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
property string configuredFontFamily: "Noto Sans"
property int configuredLabelFontSize: 16
property int configuredTitleFontSize: 28
property int configuredArtistFontSize: 26
property string configuredLabelVisibilityMode: "auto"
property string configuredLabelPlacement: "left"
property string configuredLabelText: "NOW\nPLAYING"
property bool configuredUseLabelArtwork: false
property int configuredImageBorderRadius: 8
property string configuredBackgroundStyle: "custom"
property string configuredBackgroundColor: "transparent"
property int configuredBackgroundRadius: 0
property string configuredForegroundColor: "white"
property bool configuredTextShadowEnabled: false
property bool configuredShowMediaControls: true
property int configuredTrackTextVerticalSpacing: 5
property int configuredLabelVerticalSpacing: 0
property int configuredSeparatorGapLabel: 4
property int configuredSeparatorGapTrack: 4
property int configuredSeparatorHeight: 90
property bool configuredHideSeparator: false
readonly property double buttonSize: 16
readonly property string effectiveLabelVisibilityMode: configuredLabelVisibilityMode === "always" || configuredLabelVisibilityMode === "never" ? configuredLabelVisibilityMode : "auto"
readonly property bool labelsOnRight: configuredLabelPlacement === "right"
readonly property bool hideNowPlayingArea: effectiveLabelVisibilityMode === "never"
readonly property bool usesCustomBackground: configuredBackgroundStyle === "custom"
readonly property int hideModeHorizontalPadding: 8
readonly property int labelRailWidth: 100
readonly property int effectiveImageBorderRadius: Math.max(0, configuredImageBorderRadius)
readonly property string effectiveBackgroundColor: configuredBackgroundColor || "transparent"
readonly property int effectiveBackgroundRadius: Math.max(0, configuredBackgroundRadius)
readonly property string effectiveForegroundColor: configuredForegroundColor || "white"
readonly property int effectiveLabelFontSize: Math.max(1, configuredLabelFontSize)
readonly property int effectiveTitleFontSize: Math.max(1, configuredTitleFontSize)
readonly property int effectiveArtistFontSize: Math.max(1, configuredArtistFontSize)
readonly property int effectiveTrackTextVerticalSpacing: configuredTrackTextVerticalSpacing
readonly property int effectiveLabelVerticalSpacing: configuredLabelVerticalSpacing
readonly property int effectiveSeparatorGapLabel: Math.max(0, configuredSeparatorGapLabel)
readonly property int effectiveSeparatorGapTrack: Math.max(0, configuredSeparatorGapTrack)
readonly property int effectiveSeparatorHeight: Math.max(0, Math.min(100, configuredSeparatorHeight))
readonly property var effectiveLabelLines: (configuredLabelText || "").split(/\r?\n/)
readonly property bool mediaControlsEnabled: configuredShowMediaControls
readonly property bool hasTrackInfo: player.ready && (((player.title || "").trim().length > 0) || ((player.artists || "").trim().length > 0))
readonly property bool rawHoverActive: mediaControlsEnabled && rootHoverHandler.hovered
readonly property bool controlsHoverActive: mediaControlsEnabled && (rawHoverActive || hoverExitTimer.running)
readonly property bool nowPlayingLabelsVisible: {
if (effectiveLabelVisibilityMode === "always")
return true;
if (effectiveLabelVisibilityMode === "never")
return false;
return hasTrackInfo;
}
focus: true
acceptedButtons: Qt.NoButton
hoverEnabled: true
onRawHoverActiveChanged: {
if (!mediaControlsMouseArea.mediaControlsEnabled) {
hoverExitTimer.stop();
return ;
}
if (rawHoverActive)
hoverExitTimer.stop();
else
hoverExitTimer.restart();
}
onMediaControlsEnabledChanged: {
if (!mediaControlsMouseArea.mediaControlsEnabled)
hoverExitTimer.stop();
}
Keys.onPressed: (event) => {
if (!mediaControlsMouseArea.mediaControlsEnabled) {
event.accepted = false;
return ;
}
if (!event.modifiers) {
event.accepted = true;
if (event.key === Qt.Key_Space || event.key === Qt.Key_K)
@@ -25,17 +97,42 @@ MouseArea {
}
}
HoverHandler {
id: rootHoverHandler
enabled: mediaControlsMouseArea.mediaControlsEnabled
margin: 60
}
Timer {
id: hoverExitTimer
interval: 300
}
Rectangle {
anchors.fill: parent
visible: mediaControlsMouseArea.usesCustomBackground
color: mediaControlsMouseArea.effectiveBackgroundColor
radius: mediaControlsMouseArea.effectiveBackgroundRadius
}
RowLayout {
anchors.fill: parent
layoutDirection: mediaControlsMouseArea.labelsOnRight ? Qt.RightToLeft : Qt.LeftToRight
ColumnLayout {
id: leftColumn
visible: !mediaControlsMouseArea.hideNowPlayingArea
Layout.fillHeight: true
Layout.minimumWidth: visible ? mediaControlsMouseArea.labelRailWidth : 0
Layout.preferredWidth: visible ? mediaControlsMouseArea.labelRailWidth : 0
Layout.maximumWidth: visible ? mediaControlsMouseArea.labelRailWidth : 0
states: [
State {
name: "buttonsVisible"
when: mediaControlsMouseArea.containsMouse
when: mediaControlsMouseArea.controlsHoverActive
PropertyChanges {
target: nowPlayingLabels
@@ -45,7 +142,7 @@ MouseArea {
},
State {
name: "buttonsHidden"
when: !mediaControlsMouseArea.containsMouse
when: !mediaControlsMouseArea.controlsHoverActive
PropertyChanges {
target: nowPlayingLabels
@@ -63,29 +160,23 @@ MouseArea {
ColumnLayout {
id: nowPlayingLabels
Layout.alignment: Qt.AlignRight
spacing: 0
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
Layout.fillWidth: true
visible: mediaControlsMouseArea.nowPlayingLabelsVisible
PlasmaComponents.Label {
Layout.alignment: Qt.AlignRight
text: "NOW"
lineHeight: 0.8
font.pixelSize: 16
font.bold: true
// qmllint disable missing-property
font.family: plasmoid.configuration.fontFamily
}
PlasmaComponents.Label {
id: nowPlayingLabel2
Layout.alignment: Qt.AlignRight
text: "PLAYING"
lineHeight: 0.8
font.bold: true
font.pixelSize: 16
// qmllint disable missing-property
font.family: plasmoid.configuration.fontFamily
LabelContent {
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
Layout.fillWidth: true
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
}
}
@@ -93,9 +184,10 @@ MouseArea {
RowLayout {
id: mediaControls
Layout.alignment: Qt.AlignHCenter
opacity: mediaControlsMouseArea.containsMouse ? 1 : 0
visible: opacity > 0
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
enabled: mediaControlsMouseArea.mediaControlsEnabled && mediaControlsMouseArea.controlsHoverActive
opacity: mediaControlsMouseArea.mediaControlsEnabled && mediaControlsMouseArea.controlsHoverActive && mediaControlsMouseArea.nowPlayingLabelsVisible && !mediaControlsMouseArea.hideNowPlayingArea ? 1 : 0
visible: mediaControlsMouseArea.mediaControlsEnabled && mediaControlsMouseArea.controlsHoverActive && mediaControlsMouseArea.nowPlayingLabelsVisible && !mediaControlsMouseArea.hideNowPlayingArea
QQC2.Button {
Layout.preferredWidth: buttonSize
@@ -109,6 +201,7 @@ MouseArea {
contentItem: Kirigami.Icon {
source: "media-skip-backward"
color: mediaControlsMouseArea.effectiveForegroundColor
}
}
@@ -127,6 +220,7 @@ MouseArea {
contentItem: Kirigami.Icon {
source: player.playbackStatus === 2 ? "media-playback-pause" : "media-playback-start"
color: mediaControlsMouseArea.effectiveForegroundColor
}
}
@@ -140,6 +234,7 @@ MouseArea {
contentItem: Kirigami.Icon {
source: "media-skip-forward"
color: mediaControlsMouseArea.effectiveForegroundColor
}
}
@@ -170,38 +265,60 @@ MouseArea {
}
Rectangle {
id: separator
Item {
id: separatorContainer
visible: mediaControlsMouseArea.nowPlayingLabelsVisible && !mediaControlsMouseArea.hideNowPlayingArea
Layout.fillHeight: true
Layout.leftMargin: visible ? mediaControlsMouseArea.effectiveSeparatorGapLabel : 0
Layout.rightMargin: visible ? mediaControlsMouseArea.effectiveSeparatorGapTrack : 0
Layout.minimumWidth: visible ? 1 : 0
Layout.preferredWidth: visible ? 1 : 0
Layout.maximumWidth: visible ? 1 : 0
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: 1
height: Math.round(parent.height * mediaControlsMouseArea.effectiveSeparatorHeight / 100)
color: mediaControlsMouseArea.configuredHideSeparator ? "transparent" : mediaControlsMouseArea.effectiveForegroundColor
}
}
ColumnLayout {
id: infoColumn
Layout.fillWidth: true
Layout.minimumWidth: 0
Layout.leftMargin: mediaControlsMouseArea.hideNowPlayingArea ? mediaControlsMouseArea.hideModeHorizontalPadding : 0
Layout.rightMargin: mediaControlsMouseArea.hideNowPlayingArea ? mediaControlsMouseArea.hideModeHorizontalPadding : 0
spacing: mediaControlsMouseArea.effectiveTrackTextVerticalSpacing
PlasmaComponents.Label {
ShadowedLabel {
Layout.fillWidth: true
shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
text: player.title
font.pixelSize: 28
font.pixelSize: mediaControlsMouseArea.effectiveTitleFontSize
font.bold: true
// qmllint disable missing-property
font.family: plasmoid.configuration.fontFamily
font.family: mediaControlsMouseArea.configuredFontFamily
color: mediaControlsMouseArea.effectiveForegroundColor
lineHeight: 0.8
elide: Text.ElideRight
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignRight : Text.AlignLeft
}
PlasmaComponents.Label {
Layout.maximumWidth: 300
ShadowedLabel {
Layout.fillWidth: true
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignRight : Qt.AlignLeft
shadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
text: player.artists
font.pixelSize: 26
// qmllint disable missing-property
font.family: plasmoid.configuration.fontFamily
font.pixelSize: mediaControlsMouseArea.effectiveArtistFontSize
font.family: mediaControlsMouseArea.configuredFontFamily
color: mediaControlsMouseArea.effectiveForegroundColor
lineHeight: 0.8
elide: Text.ElideRight
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignRight : Text.AlignLeft
}
}
+61
View File
@@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Effects
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
readonly property real contentWidth: width > 0 ? width : foregroundLabel.implicitWidth
readonly property real contentHeight: height > 0 ? height : foregroundLabel.implicitHeight
implicitWidth: foregroundLabel.implicitWidth
implicitHeight: foregroundLabel.implicitHeight
PlasmaComponents.Label {
id: shadowSourceLabel
visible: false
width: shadowedLabel.contentWidth
height: shadowedLabel.contentHeight
text: foregroundLabel.text
font: foregroundLabel.font
lineHeight: foregroundLabel.lineHeight
elide: foregroundLabel.elide
horizontalAlignment: foregroundLabel.horizontalAlignment
verticalAlignment: foregroundLabel.verticalAlignment
color: "white"
}
MultiEffect {
x: shadowSourceLabel.x
y: shadowSourceLabel.y
width: shadowSourceLabel.width
height: shadowSourceLabel.height
autoPaddingEnabled: true
shadowBlur: 0.5
shadowColor: "#3f4c66"
shadowEnabled: shadowedLabel.shadowEnabled
shadowHorizontalOffset: 0
shadowOpacity: 0.42
shadowScale: 1
shadowVerticalOffset: 2
source: shadowSourceLabel
visible: shadowedLabel.shadowEnabled
}
PlasmaComponents.Label {
id: foregroundLabel
width: shadowedLabel.contentWidth
height: shadowedLabel.contentHeight
}
}
+325
View File
@@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Dialogs as Dialogs
import QtQuick.Layouts
import org.kde.kcmutils as KCM
import org.kde.kirigami as Kirigami
@@ -9,16 +10,97 @@ KCM.SimpleKCM {
property alias cfg_opacity: opacitySpinBox.value
property string cfg_fontFamily
property alias cfg_labelFontSize: labelFontSizeSpinBox.value
property alias cfg_titleFontSize: titleFontSizeSpinBox.value
property alias cfg_artistFontSize: artistFontSizeSpinBox.value
property string cfg_labelVisibilityMode
property string cfg_labelPlacement
property alias cfg_labelText: labelTextArea.text
property alias cfg_useLabelArtwork: useLabelArtworkCheckBox.checked
property alias cfg_imageBorderRadius: imageBorderRadiusSpinBox.value
property string cfg_backgroundStyle
property alias cfg_backgroundColor: backgroundColorField.text
property alias cfg_backgroundRadius: backgroundRadiusSpinBox.value
property alias cfg_foregroundColor: foregroundColorField.text
property alias cfg_textShadowEnabled: textShadowCheckBox.checked
property alias cfg_showMediaControls: showMediaControlsCheckBox.checked
property alias cfg_trackTextVerticalSpacing: trackTextVerticalSpacingSpinBox.value
property alias cfg_labelVerticalSpacing: labelVerticalSpacingSpinBox.value
property alias cfg_separatorGapLabel: separatorGapLabelSpinBox.value
property alias cfg_separatorGapTrack: separatorGapTrackSpinBox.value
property alias cfg_separatorHeight: separatorHeightSpinBox.value
property alias cfg_hideSeparator: hideSeparatorCheckBox.checked
readonly property var availableFonts: Qt.fontFamilies()
readonly property var labelVisibilityOptions: [{
"text": i18n("Auto hide when idle"),
"value": "auto"
}, {
"text": i18n("Always show"),
"value": "always"
}, {
"text": i18n("Always hide"),
"value": "never"
}]
readonly property var labelPlacementOptions: [{
"text": i18n("Left"),
"value": "left"
}, {
"text": i18n("Right"),
"value": "right"
}]
readonly property var backgroundStyleOptions: [{
"text": i18n("Default"),
"value": "custom"
}, {
"text": i18n("Solid background"),
"value": "default"
}]
function syncComboByValue(comboBox, options, configuredValue, fallbackValue) {
const requestedValue = configuredValue || fallbackValue;
const requestedIndex = options.findIndex((option) => {
return option.value === requestedValue;
});
const fallbackIndex = options.findIndex((option) => {
return option.value === fallbackValue;
});
const resolvedIndex = requestedIndex >= 0 ? requestedIndex : (fallbackIndex >= 0 ? fallbackIndex : 0);
if (comboBox.currentIndex !== resolvedIndex)
comboBox.currentIndex = resolvedIndex;
return options[resolvedIndex].value;
}
onCfg_fontFamilyChanged: {
const index = availableFonts.indexOf(cfg_fontFamily);
if (index >= 0 && fontFamilyComboBox.currentIndex !== index)
fontFamilyComboBox.currentIndex = index;
}
onCfg_labelVisibilityModeChanged: {
const resolvedValue = syncComboByValue(labelVisibilityModeComboBox, labelVisibilityOptions, cfg_labelVisibilityMode, "auto");
if (cfg_labelVisibilityMode !== resolvedValue)
cfg_labelVisibilityMode = resolvedValue;
}
onCfg_labelPlacementChanged: {
const resolvedValue = syncComboByValue(labelPlacementComboBox, labelPlacementOptions, cfg_labelPlacement, "left");
if (cfg_labelPlacement !== resolvedValue)
cfg_labelPlacement = resolvedValue;
}
onCfg_backgroundStyleChanged: {
const resolvedValue = syncComboByValue(backgroundStyleComboBox, backgroundStyleOptions, cfg_backgroundStyle, "custom");
if (cfg_backgroundStyle !== resolvedValue)
cfg_backgroundStyle = resolvedValue;
}
Kirigami.FormLayout {
SectionHeader {
title: i18n("Global")
}
QQC2.SpinBox {
id: opacitySpinBox
@@ -35,6 +117,249 @@ KCM.SimpleKCM {
onActivated: configRoot.cfg_fontFamily = currentText
}
QQC2.ComboBox {
id: backgroundStyleComboBox
Kirigami.FormData.label: i18n("Background style:")
model: configRoot.backgroundStyleOptions
textRole: "text"
valueRole: "value"
onActivated: configRoot.cfg_backgroundStyle = currentValue
}
RowLayout {
Kirigami.FormData.label: i18n("Background color:")
enabled: configRoot.cfg_backgroundStyle === "custom"
QQC2.TextField {
id: backgroundColorField
Layout.fillWidth: true
placeholderText: i18n("transparent or #AARRGGBB")
}
QQC2.Button {
text: i18n("Pick...")
onClicked: {
backgroundColorDialog.selectedColor = backgroundColorField.text || "transparent";
backgroundColorDialog.open();
}
}
}
RowLayout {
Kirigami.FormData.label: i18n("Foreground color:")
QQC2.TextField {
id: foregroundColorField
Layout.fillWidth: true
placeholderText: i18n("white or #RRGGBB")
}
QQC2.Button {
text: i18n("Pick...")
onClicked: {
foregroundColorDialog.selectedColor = foregroundColorField.text || "white";
foregroundColorDialog.open();
}
}
}
QQC2.SpinBox {
id: backgroundRadiusSpinBox
Kirigami.FormData.label: i18n("Background radius:")
enabled: configRoot.cfg_backgroundStyle === "custom"
from: 0
to: 100
}
QQC2.CheckBox {
id: textShadowCheckBox
Kirigami.FormData.label: i18n("Text shadow:")
text: i18n("Enable")
}
QQC2.CheckBox {
id: showMediaControlsCheckBox
Kirigami.FormData.label: i18n("Media controls:")
text: i18n("Show media controls")
}
QQC2.SpinBox {
id: separatorHeightSpinBox
Kirigami.FormData.label: i18n("Separator height (%):")
from: 0
to: 100
}
QQC2.CheckBox {
id: hideSeparatorCheckBox
Kirigami.FormData.label: i18n("Separator:")
text: i18n("Hide separator")
}
SectionHeader {
title: i18n("Label")
}
QQC2.ComboBox {
id: labelVisibilityModeComboBox
Kirigami.FormData.label: i18n("Now Playing labels:")
model: configRoot.labelVisibilityOptions
textRole: "text"
valueRole: "value"
onActivated: configRoot.cfg_labelVisibilityMode = currentValue
}
QQC2.ComboBox {
id: labelPlacementComboBox
Kirigami.FormData.label: i18n("Label placement:")
model: configRoot.labelPlacementOptions
textRole: "text"
valueRole: "value"
onActivated: configRoot.cfg_labelPlacement = currentValue
}
QQC2.CheckBox {
id: useLabelArtworkCheckBox
Kirigami.FormData.label: i18n("Label image:")
text: i18n("Show track image when available")
}
QQC2.SpinBox {
id: imageBorderRadiusSpinBox
Kirigami.FormData.label: i18n("Image border radius:")
from: 0
to: 100
}
QQC2.TextArea {
id: labelTextArea
Kirigami.FormData.label: i18n("Label text:")
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 3
placeholderText: i18n("Use a line break for multiple lines")
wrapMode: TextEdit.NoWrap
}
QQC2.SpinBox {
id: labelFontSizeSpinBox
Kirigami.FormData.label: i18n("Label font size:")
from: 8
to: 72
}
QQC2.SpinBox {
id: labelVerticalSpacingSpinBox
Kirigami.FormData.label: i18n("Label line spacing (negatives possible):")
from: -200
to: 200
}
QQC2.SpinBox {
id: separatorGapLabelSpinBox
Kirigami.FormData.label: i18n("Separator gap from label:")
from: 0
to: 24
}
SectionHeader {
title: i18n("Track & Artist")
}
QQC2.SpinBox {
id: titleFontSizeSpinBox
Kirigami.FormData.label: i18n("Title font size:")
from: 8
to: 96
}
QQC2.SpinBox {
id: artistFontSizeSpinBox
Kirigami.FormData.label: i18n("Artist font size:")
from: 8
to: 96
}
QQC2.SpinBox {
id: trackTextVerticalSpacingSpinBox
Kirigami.FormData.label: i18n("Title/artist spacing (negatives possible):")
from: -200
to: 200
}
QQC2.SpinBox {
id: separatorGapTrackSpinBox
Kirigami.FormData.label: i18n("Separator gap from track:")
from: 0
to: 24
}
}
Dialogs.ColorDialog {
id: backgroundColorDialog
title: i18n("Select background color")
options: Dialogs.ColorDialog.ShowAlphaChannel
onAccepted: configRoot.cfg_backgroundColor = selectedColor.toString()
}
Dialogs.ColorDialog {
id: foregroundColorDialog
title: i18n("Select foreground color")
options: Dialogs.ColorDialog.ShowAlphaChannel
onAccepted: configRoot.cfg_foregroundColor = selectedColor.toString()
}
component SectionHeader: Item {
required property string title
Kirigami.FormData.isSection: true
implicitHeight: sectionLayout.implicitHeight
ColumnLayout {
id: sectionLayout
anchors.left: parent.left
anchors.right: parent.right
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
text: parent.parent.title
font.bold: true
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: Kirigami.Theme.textColor
opacity: 0.25
}
}
}
}
+102 -1
View File
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.plasmoid
PlasmoidItem {
@@ -10,12 +11,64 @@ PlasmoidItem {
// "qmllint" sees `plasmoid` as QObject here, but Plasma injects `configuration` at runtime.
// qmllint disable missing-property
property int configuredOpacity: plasmoid.configuration.opacity
// qmllint disable missing-property
property string configuredFontFamily: plasmoid.configuration.fontFamily
// qmllint disable missing-property
property int configuredLabelFontSize: plasmoid.configuration.labelFontSize
// qmllint disable missing-property
property int configuredTitleFontSize: plasmoid.configuration.titleFontSize
// qmllint disable missing-property
property int configuredArtistFontSize: plasmoid.configuration.artistFontSize
// qmllint disable missing-property
property string configuredLabelVisibilityMode: plasmoid.configuration.labelVisibilityMode
// qmllint disable missing-property
// qmllint disable missing-property
property string configuredLabelPlacement: plasmoid.configuration.labelPlacement
// qmllint disable missing-property
property string configuredLabelText: plasmoid.configuration.labelText
// qmllint disable missing-property
property bool configuredUseLabelArtwork: plasmoid.configuration.useLabelArtwork
// qmllint disable missing-property
property int configuredImageBorderRadius: plasmoid.configuration.imageBorderRadius
// qmllint disable missing-property
property string configuredBackgroundStyle: plasmoid.configuration.backgroundStyle
// qmllint disable missing-property
property string configuredBackgroundColor: plasmoid.configuration.backgroundColor
// qmllint disable missing-property
property int configuredBackgroundRadius: plasmoid.configuration.backgroundRadius
// qmllint disable missing-property
property string configuredForegroundColor: plasmoid.configuration.foregroundColor
// qmllint disable missing-property
property bool configuredTextShadowEnabled: plasmoid.configuration.textShadowEnabled
// qmllint disable missing-property
property bool configuredShowMediaControls: plasmoid.configuration.showMediaControls
// qmllint disable missing-property
property int configuredTrackTextVerticalSpacing: plasmoid.configuration.trackTextVerticalSpacing
// qmllint disable missing-property
property int configuredLabelVerticalSpacing: plasmoid.configuration.labelVerticalSpacing
// qmllint disable missing-property
property int configuredSeparatorGapLabel: plasmoid.configuration.separatorGapLabel
// qmllint disable missing-property
property int configuredSeparatorGapTrack: plasmoid.configuration.separatorGapTrack
// qmllint disable missing-property
property int configuredSeparatorHeight: plasmoid.configuration.separatorHeight
// qmllint disable missing-property
property bool configuredHideSeparator: plasmoid.configuration.hideSeparator
readonly property int resolvedBackgroundHints: {
if (configuredBackgroundStyle === "default")
return PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground;
if (configuredBackgroundStyle === "none")
return PlasmaCore.Types.NoBackground;
return PlasmaCore.Types.NoBackground;
}
width: Kirigami.Units.gridUnit * 25
height: Kirigami.Units.gridUnit * 5
Layout.minimumWidth: Kirigami.Units.gridUnit * 25
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Plasmoid.backgroundHints: "NoBackground"
Plasmoid.backgroundHints: resolvedBackgroundHints
opacity: configuredOpacity / 100
Player {
@@ -23,15 +76,63 @@ PlasmoidItem {
}
fullRepresentation: Representation {
width: root.width
height: root.height
Layout.minimumWidth: Kirigami.Units.gridUnit * 25
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: root.width
Layout.preferredHeight: root.height
configuredFontFamily: root.configuredFontFamily
configuredLabelFontSize: root.configuredLabelFontSize
configuredTitleFontSize: root.configuredTitleFontSize
configuredArtistFontSize: root.configuredArtistFontSize
configuredLabelVisibilityMode: root.configuredLabelVisibilityMode
configuredLabelPlacement: root.configuredLabelPlacement
configuredLabelText: root.configuredLabelText
configuredUseLabelArtwork: root.configuredUseLabelArtwork
configuredImageBorderRadius: root.configuredImageBorderRadius
configuredBackgroundStyle: root.configuredBackgroundStyle
configuredBackgroundColor: root.configuredBackgroundColor
configuredBackgroundRadius: root.configuredBackgroundRadius
configuredForegroundColor: root.configuredForegroundColor
configuredTextShadowEnabled: root.configuredTextShadowEnabled
configuredShowMediaControls: root.configuredShowMediaControls
configuredTrackTextVerticalSpacing: root.configuredTrackTextVerticalSpacing
configuredLabelVerticalSpacing: root.configuredLabelVerticalSpacing
configuredSeparatorGapLabel: root.configuredSeparatorGapLabel
configuredSeparatorGapTrack: root.configuredSeparatorGapTrack
configuredSeparatorHeight: root.configuredSeparatorHeight
configuredHideSeparator: root.configuredHideSeparator
}
compactRepresentation: Representation {
width: root.width
height: root.height
Layout.minimumWidth: Kirigami.Units.gridUnit * 25
Layout.minimumHeight: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: root.width
Layout.preferredHeight: root.height
configuredFontFamily: root.configuredFontFamily
configuredLabelFontSize: root.configuredLabelFontSize
configuredTitleFontSize: root.configuredTitleFontSize
configuredArtistFontSize: root.configuredArtistFontSize
configuredLabelVisibilityMode: root.configuredLabelVisibilityMode
configuredLabelPlacement: root.configuredLabelPlacement
configuredLabelText: root.configuredLabelText
configuredUseLabelArtwork: root.configuredUseLabelArtwork
configuredImageBorderRadius: root.configuredImageBorderRadius
configuredBackgroundStyle: root.configuredBackgroundStyle
configuredBackgroundColor: root.configuredBackgroundColor
configuredBackgroundRadius: root.configuredBackgroundRadius
configuredForegroundColor: root.configuredForegroundColor
configuredTextShadowEnabled: root.configuredTextShadowEnabled
configuredShowMediaControls: root.configuredShowMediaControls
configuredTrackTextVerticalSpacing: root.configuredTrackTextVerticalSpacing
configuredLabelVerticalSpacing: root.configuredLabelVerticalSpacing
configuredSeparatorGapLabel: root.configuredSeparatorGapLabel
configuredSeparatorGapTrack: root.configuredSeparatorGapTrack
configuredSeparatorHeight: root.configuredSeparatorHeight
configuredHideSeparator: root.configuredHideSeparator
}
}