6 Commits

6 changed files with 264 additions and 122 deletions
+8
View File
@@ -70,6 +70,14 @@ PLAYING]]></default>
<label>Show media controls</label>
<default>true</default>
</entry>
<entry name="sourcePriority" type="String">
<label>Source priority</label>
<default></default>
</entry>
<entry name="sourceWhitelist" type="Bool">
<label>Only use listed sources</label>
<default>false</default>
</entry>
<entry name="trackTextVerticalSpacing" type="Int">
<label>Track text vertical spacing</label>
<default>5</default>
+5 -3
View File
@@ -7,13 +7,13 @@ Item {
required property string artworkSource
required property var labelLines
required property string fontFamily
required property int fontPixelSize
required property real 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
required property real artworkSize
property string currentArtworkSource: ""
property string incomingArtworkSource: ""
property real currentArtworkOpacity: 0
@@ -104,7 +104,8 @@ Item {
}
clip: holdsArtworkSlot
implicitHeight: holdsArtworkSlot ? artworkSize : labelTextColumn.implicitHeight
implicitWidth: artworkSize
implicitHeight: artworkSize
onArtworkSourceChanged: handleArtworkSourceChange()
Component.onCompleted: handleArtworkSourceChange()
@@ -184,6 +185,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: labelContent.verticalSpacing
opacity: labelContent.textOpacity
visible: opacity > 0
+116 -41
View File
@@ -11,82 +11,159 @@ import org.kde.plasma.private.mpris as Mpris
QtObject {
id: root
readonly property int containerRole: Qt.UserRole + 1
property string sourceName: "any"
property string sourcePriority: ""
property bool sourceWhitelist: false
property var mpris2Model
property int modelRevision: 0
readonly property var normalizedSourcePriority: parseSourcePriority(sourcePriority)
readonly property var resolvedPlayer: resolvePlayer()
readonly property bool ready: {
if (!mpris2Model.currentPlayer)
if (!resolvedPlayer)
return false;
return mpris2Model.currentPlayer.desktopEntry === sourceName || sourceName === "any";
return sourceName === "any" || entriesMatch(resolvedPlayer.desktopEntry, sourceName);
}
readonly property string artists: ready ? mpris2Model.currentPlayer.artist : ""
readonly property string title: ready ? mpris2Model.currentPlayer.track : ""
readonly property int playbackStatus: ready ? mpris2Model.currentPlayer.playbackStatus : Mpris.PlaybackStatus.Unknown
readonly property int shuffle: ready ? mpris2Model.currentPlayer.shuffle : Mpris.ShuffleStatus.Unknown
readonly property string artUrl: ready ? mpris2Model.currentPlayer.artUrl : ""
readonly property int loopStatus: ready ? mpris2Model.currentPlayer.loopStatus : Mpris.LoopStatus.Unknown
readonly property double songPosition: ready ? mpris2Model.currentPlayer.position : 0
readonly property double songLength: ready ? mpris2Model.currentPlayer.length : 0
readonly property real volume: ready ? mpris2Model.currentPlayer.volume : 0
readonly property string identity: ready ? mpris2Model.currentPlayer.identity : ""
readonly property bool canGoNext: ready ? mpris2Model.currentPlayer.canGoNext : false
readonly property bool canGoPrevious: ready ? mpris2Model.currentPlayer.canGoPrevious : false
readonly property bool canPlay: ready ? mpris2Model.currentPlayer.canPlay : false
readonly property bool canPause: ready ? mpris2Model.currentPlayer.canPause : false
readonly property bool canSeek: ready ? mpris2Model.currentPlayer.canSeek : false
readonly property bool canRaise: ready ? mpris2Model.currentPlayer.canRaise : false
readonly property string artists: ready ? resolvedPlayer.artist : ""
readonly property string title: ready ? resolvedPlayer.track : ""
readonly property int playbackStatus: ready ? resolvedPlayer.playbackStatus : Mpris.PlaybackStatus.Unknown
readonly property int shuffle: ready ? resolvedPlayer.shuffle : Mpris.ShuffleStatus.Unknown
readonly property string artUrl: ready ? resolvedPlayer.artUrl : ""
readonly property int loopStatus: ready ? resolvedPlayer.loopStatus : Mpris.LoopStatus.Unknown
readonly property double songPosition: ready ? resolvedPlayer.position : 0
readonly property double songLength: ready ? resolvedPlayer.length : 0
readonly property real volume: ready ? resolvedPlayer.volume : 0
readonly property string identity: ready ? resolvedPlayer.identity : ""
readonly property bool canGoNext: ready ? resolvedPlayer.canGoNext : false
readonly property bool canGoPrevious: ready ? resolvedPlayer.canGoPrevious : false
readonly property bool canPlay: ready ? resolvedPlayer.canPlay : false
readonly property bool canPause: ready ? resolvedPlayer.canPause : false
readonly property bool canSeek: ready ? resolvedPlayer.canSeek : false
readonly property bool canRaise: ready ? resolvedPlayer.canRaise : false
// To know whether Shuffle and Loop can be changed we have to check if the property is defined,
// unlike the other commands, LoopStatus and Shuffle do not have specific properties such as
// CanPause, CanSeek, etc.
readonly property bool canChangeShuffle: ready ? mpris2Model.currentPlayer.shuffle !== undefined : false
readonly property bool canChangeLoopStatus: ready ? mpris2Model.currentPlayer.loopStatus !== undefined : false
readonly property bool canChangeShuffle: ready ? resolvedPlayer.shuffle !== undefined : false
readonly property bool canChangeLoopStatus: ready ? resolvedPlayer.loopStatus !== undefined : false
function normalizedEntry(value) {
return (value || "").toString().trim().toLowerCase();
}
function entriesMatch(left, right) {
const normalizedLeft = normalizedEntry(left);
const normalizedRight = normalizedEntry(right);
if (!normalizedLeft || !normalizedRight)
return false;
return normalizedLeft === normalizedRight;
}
function parseSourcePriority(value) {
return (value || "").split(",").map((entry) => {
return normalizedEntry(entry);
}).filter((entry) => {
return entry.length > 0;
});
}
function playerAt(row) {
if (!mpris2Model || row < 0 || row >= mpris2Model.rowCount())
return null;
return mpris2Model.data(mpris2Model.index(row, 0), containerRole);
}
function findPlayerByEntry(targetEntry) {
if (!targetEntry || !mpris2Model)
return null;
const playerCount = mpris2Model.rowCount();
for (let row = 0; row < playerCount; ++row) {
const player = playerAt(row);
if (player && entriesMatch(player.desktopEntry, targetEntry))
return player;
}
return null;
}
function resolvePlayer() {
modelRevision;
if (!mpris2Model)
return null;
for (const targetEntry of normalizedSourcePriority) {
const matchedPlayer = findPlayerByEntry(targetEntry);
if (matchedPlayer)
return matchedPlayer;
}
if (sourceWhitelist && normalizedSourcePriority.length > 0)
return null;
return mpris2Model.currentPlayer || null;
}
function togglePlayPause() {
if (mpris2Model.currentPlayer)
mpris2Model.currentPlayer.PlayPause();
if (resolvedPlayer)
resolvedPlayer.PlayPause();
}
function setPosition(position) {
mpris2Model.currentPlayer.position = position;
if (resolvedPlayer)
resolvedPlayer.position = position;
}
function next() {
if (mpris2Model.currentPlayer)
mpris2Model.currentPlayer.Next();
if (resolvedPlayer)
resolvedPlayer.Next();
}
function previous() {
if (mpris2Model.currentPlayer)
mpris2Model.currentPlayer.Previous();
if (resolvedPlayer)
resolvedPlayer.Previous();
}
function updatePosition() {
if (mpris2Model.currentPlayer)
mpris2Model.currentPlayer.updatePosition();
if (resolvedPlayer)
resolvedPlayer.updatePosition();
}
function setVolume(volume) {
mpris2Model.currentPlayer.volume = volume;
if (resolvedPlayer)
resolvedPlayer.volume = volume;
}
function changeVolume(delta, showOSD) {
mpris2Model.currentPlayer.changeVolume(delta, showOSD);
if (resolvedPlayer)
resolvedPlayer.changeVolume(delta, showOSD);
}
function setShuffle(shuffle) {
mpris2Model.currentPlayer.shuffle = shuffle;
if (resolvedPlayer)
resolvedPlayer.shuffle = shuffle;
}
function setLoopStatus(loopStatus) {
mpris2Model.currentPlayer.loopStatus = loopStatus;
if (resolvedPlayer)
resolvedPlayer.loopStatus = loopStatus;
}
function raise() {
mpris2Model.currentPlayer.Raise();
if (resolvedPlayer)
resolvedPlayer.Raise();
}
function formatTrackTime(s) {
@@ -99,13 +176,11 @@ QtObject {
}
mpris2Model: Mpris.Mpris2Model {
onRowsInserted: (_, rowIndex) => {
const CONTAINER_ROLE = Qt.UserRole + 1;
const player = this.data(this.index(rowIndex, 0), CONTAINER_ROLE);
if (player.desktopEntry === root.sourceName)
this.currentIndex = rowIndex;
}
onRowsInserted: root.modelRevision += 1
onRowsRemoved: root.modelRevision += 1
onModelReset: root.modelRevision += 1
onDataChanged: root.modelRevision += 1
onCurrentPlayerChanged: root.modelRevision += 1
}
}
+59 -78
View File
@@ -33,7 +33,23 @@ MouseArea {
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 contentInset: 8
readonly property int availableContentWidth: Math.max(0, width - contentInset * 2)
readonly property int separatorReservedWidth: nowPlayingLabelsVisible && !hideNowPlayingArea ? 1 + effectiveSeparatorGapLabel + effectiveSeparatorGapTrack : 0
readonly property int maxLabelRailSize: Math.max(0, availableContentWidth - separatorReservedWidth)
readonly property int labelRailSize: Math.max(0, Math.min(height - contentInset * 2, maxLabelRailSize))
readonly property bool mediaControlsTargetShown: mediaControlsEnabled && controlsHoverActive && nowPlayingLabelsVisible && !hideNowPlayingArea
readonly property int artworkControlsGap: configuredUseLabelArtwork && player.artUrl.length > 0 ? 2 : 0
readonly property real labelShrunkScale: labelRailSize > 0 ? Math.max(0, (labelRailSize - buttonSize - artworkControlsGap) / labelRailSize) : 1
property real labelContentScale: mediaControlsTargetShown ? labelShrunkScale : 1
readonly property real controlsReveal: {
const range = 1 - labelShrunkScale;
if (range <= 0)
return mediaControlsTargetShown ? 1 : 0;
return Math.max(0, Math.min(1, (1 - labelContentScale) / range));
}
readonly property real labelContentSize: labelRailSize * labelContentScale
readonly property int effectiveImageBorderRadius: Math.max(0, configuredImageBorderRadius)
readonly property string effectiveBackgroundColor: configuredBackgroundColor || "transparent"
readonly property int effectiveBackgroundRadius: Math.max(0, configuredBackgroundRadius)
@@ -67,7 +83,7 @@ MouseArea {
onRawHoverActiveChanged: {
if (!mediaControlsMouseArea.mediaControlsEnabled) {
hoverExitTimer.stop();
return ;
return;
}
if (rawHoverActive)
hoverExitTimer.stop();
@@ -77,12 +93,11 @@ MouseArea {
onMediaControlsEnabledChanged: {
if (!mediaControlsMouseArea.mediaControlsEnabled)
hoverExitTimer.stop();
}
Keys.onPressed: (event) => {
Keys.onPressed: event => {
if (!mediaControlsMouseArea.mediaControlsEnabled) {
event.accepted = false;
return ;
return;
}
if (!event.modifiers) {
event.accepted = true;
@@ -110,6 +125,13 @@ MouseArea {
interval: 300
}
Behavior on labelContentScale {
NumberAnimation {
duration: 250
easing.type: Easing.InOutQuad
}
}
Rectangle {
anchors.fill: parent
visible: mediaControlsMouseArea.usesCustomBackground
@@ -119,75 +141,65 @@ MouseArea {
RowLayout {
anchors.fill: parent
anchors.margins: mediaControlsMouseArea.contentInset
layoutDirection: mediaControlsMouseArea.labelsOnRight ? Qt.RightToLeft : Qt.LeftToRight
ColumnLayout {
Item {
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.controlsHoverActive
PropertyChanges {
target: nowPlayingLabels
Layout.bottomMargin: 10
}
},
State {
name: "buttonsHidden"
when: !mediaControlsMouseArea.controlsHoverActive
PropertyChanges {
target: nowPlayingLabels
Layout.bottomMargin: 0
}
}
]
Layout.minimumWidth: visible ? mediaControlsMouseArea.labelRailSize : 0
Layout.preferredWidth: visible ? mediaControlsMouseArea.labelRailSize : 0
Layout.maximumWidth: visible ? mediaControlsMouseArea.labelRailSize : 0
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
id: labelSlot
ColumnLayout {
id: nowPlayingLabels
readonly property bool showsArtwork: mediaControlsMouseArea.configuredUseLabelArtwork && player.artUrl.length > 0
// Text is rasterized at whole pixel sizes, then lightly scaled between steps for smoother motion.
readonly property real textTargetFontSize: Math.max(1, mediaControlsMouseArea.effectiveLabelFontSize * mediaControlsMouseArea.labelContentScale)
readonly property int textRasterFontSize: Math.max(1, Math.round(textTargetFontSize))
readonly property real textResidualScale: textTargetFontSize / textRasterFontSize
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
Layout.fillWidth: true
x: mediaControlsMouseArea.labelsOnRight ? 0 : leftColumn.width - width
y: 0
width: mediaControlsMouseArea.labelContentSize
height: mediaControlsMouseArea.labelContentSize
clip: true
visible: mediaControlsMouseArea.nowPlayingLabelsVisible
LabelContent {
Layout.alignment: mediaControlsMouseArea.labelsOnRight ? Qt.AlignLeft : Qt.AlignRight
Layout.fillWidth: true
anchors.left: parent.left
anchors.top: parent.top
width: labelSlot.showsArtwork ? mediaControlsMouseArea.labelRailSize : mediaControlsMouseArea.labelContentSize / labelSlot.textResidualScale
height: labelSlot.showsArtwork ? mediaControlsMouseArea.labelRailSize : mediaControlsMouseArea.labelContentSize / labelSlot.textResidualScale
scale: labelSlot.showsArtwork ? mediaControlsMouseArea.labelContentScale : labelSlot.textResidualScale
transformOrigin: Item.TopLeft
artworkSource: mediaControlsMouseArea.configuredUseLabelArtwork ? player.artUrl : ""
labelLines: mediaControlsMouseArea.effectiveLabelLines
fontFamily: mediaControlsMouseArea.configuredFontFamily
fontPixelSize: mediaControlsMouseArea.effectiveLabelFontSize
fontPixelSize: labelSlot.showsArtwork ? mediaControlsMouseArea.effectiveLabelFontSize : labelSlot.textRasterFontSize
textColor: mediaControlsMouseArea.effectiveForegroundColor
textShadowEnabled: mediaControlsMouseArea.configuredTextShadowEnabled
verticalSpacing: mediaControlsMouseArea.effectiveLabelVerticalSpacing
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignLeft : Text.AlignRight
borderRadius: mediaControlsMouseArea.effectiveImageBorderRadius
artworkSize: mediaControlsMouseArea.labelRailWidth
artworkSize: labelSlot.showsArtwork ? mediaControlsMouseArea.labelRailSize : mediaControlsMouseArea.labelContentSize / labelSlot.textResidualScale
}
}
RowLayout {
id: mediaControls
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
x: mediaControlsMouseArea.labelsOnRight ? 0 : leftColumn.width - width
y: mediaControlsMouseArea.labelContentSize + mediaControlsMouseArea.artworkControlsGap
width: implicitWidth
height: mediaControlsMouseArea.buttonSize
clip: true
enabled: mediaControlsMouseArea.mediaControlsTargetShown
opacity: mediaControlsMouseArea.controlsReveal
visible: mediaControlsMouseArea.mediaControlsTargetShown || opacity > 0
QQC2.Button {
Layout.preferredWidth: buttonSize
@@ -203,7 +215,6 @@ MouseArea {
source: "media-skip-backward"
color: mediaControlsMouseArea.effectiveForegroundColor
}
}
QQC2.Button {
@@ -222,7 +233,6 @@ MouseArea {
source: player.playbackStatus === 2 ? "media-playback-pause" : "media-playback-start"
color: mediaControlsMouseArea.effectiveForegroundColor
}
}
QQC2.Button {
@@ -236,33 +246,8 @@ MouseArea {
source: "media-skip-forward"
color: mediaControlsMouseArea.effectiveForegroundColor
}
}
Behavior on opacity {
PropertyAnimation {
duration: 250
easing.type: Easing.InOutQuad
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
transitions: Transition {
NumberAnimation {
duration: 250
easing.type: Easing.InOutQuad
properties: "Layout.bottomMargin"
}
}
}
Item {
@@ -283,7 +268,6 @@ MouseArea {
height: Math.round(parent.height * mediaControlsMouseArea.effectiveSeparatorHeight / 100)
color: mediaControlsMouseArea.configuredHideSeparator ? "transparent" : mediaControlsMouseArea.effectiveForegroundColor
}
}
ColumnLayout {
@@ -320,9 +304,6 @@ MouseArea {
elide: Text.ElideRight
horizontalAlignment: mediaControlsMouseArea.labelsOnRight ? Text.AlignRight : Text.AlignLeft
}
}
}
}
+69
View File
@@ -4,6 +4,7 @@ import QtQuick.Dialogs as Dialogs
import QtQuick.Layouts
import org.kde.kcmutils as KCM
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mpris as Mpris
KCM.SimpleKCM {
id: configRoot
@@ -24,13 +25,17 @@ KCM.SimpleKCM {
property alias cfg_foregroundColor: foregroundColorField.text
property alias cfg_textShadowEnabled: textShadowCheckBox.checked
property alias cfg_showMediaControls: showMediaControlsCheckBox.checked
property alias cfg_sourcePriority: sourcePriorityField.text
property alias cfg_sourceWhitelist: sourceWhitelistCheckBox.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 int containerRole: Qt.UserRole + 1
readonly property var availableFonts: Qt.fontFamilies()
property var detectedSources: []
readonly property var labelVisibilityOptions: [{
"text": i18n("Auto hide when idle"),
"value": "auto"
@@ -55,6 +60,9 @@ KCM.SimpleKCM {
"text": i18n("Solid background"),
"value": "default"
}]
readonly property bool hasSourcePriorityEntries: sourcePriorityField.text.split(",").some((entry) => {
return normalizeDesktopEntry(entry).length > 0;
})
function syncComboByValue(comboBox, options, configuredValue, fallbackValue) {
const requestedValue = configuredValue || fallbackValue;
@@ -71,6 +79,29 @@ KCM.SimpleKCM {
return options[resolvedIndex].value;
}
function normalizeDesktopEntry(value) {
return (value || "").toString().trim();
}
function refreshDetectedSources() {
const sources = [];
const seen = {
};
const playerCount = detectedSourcesModel.rowCount();
for (let row = 0; row < playerCount; ++row) {
const player = detectedSourcesModel.data(detectedSourcesModel.index(row, 0), containerRole);
const desktopEntry = normalizeDesktopEntry(player ? player.desktopEntry : "");
const desktopEntryKey = desktopEntry.toLowerCase();
if (!desktopEntry || seen[desktopEntryKey])
continue;
seen[desktopEntryKey] = true;
sources.push(desktopEntry);
}
detectedSources = sources;
}
Component.onCompleted: cfg_sourceWhitelist = hasSourcePriorityEntries && cfg_sourceWhitelist
onCfg_fontFamilyChanged: {
const index = availableFonts.indexOf(cfg_fontFamily);
if (index >= 0 && fontFamilyComboBox.currentIndex !== index)
@@ -191,6 +222,34 @@ KCM.SimpleKCM {
text: i18n("Show media controls")
}
QQC2.TextField {
id: sourcePriorityField
Kirigami.FormData.label: i18n("Source priority:")
Layout.fillWidth: true
placeholderText: i18n("example: spotify, firefox")
onTextChanged: configRoot.cfg_sourceWhitelist = configRoot.hasSourcePriorityEntries && configRoot.cfg_sourceWhitelist
}
QQC2.CheckBox {
id: sourceWhitelistCheckBox
Kirigami.FormData.label: ""
text: i18n("Only use listed sources")
enabled: configRoot.hasSourcePriorityEntries
}
QQC2.Label {
id: detectedSourcesField
Kirigami.FormData.label: i18n("Current sources being detected:")
Layout.fillWidth: true
Layout.preferredHeight: Math.max(implicitHeight, Kirigami.Units.gridUnit * 2)
textFormat: TextEdit.PlainText
wrapMode: TextEdit.WrapAnywhere
text: configRoot.detectedSources.length > 0 ? configRoot.detectedSources.join("\n") : i18n("No active MPRIS desktopEntry values detected")
}
QQC2.SpinBox {
id: separatorHeightSpinBox
@@ -333,6 +392,16 @@ KCM.SimpleKCM {
onAccepted: configRoot.cfg_foregroundColor = selectedColor.toString()
}
Mpris.Mpris2Model {
id: detectedSourcesModel
onRowsInserted: configRoot.refreshDetectedSources()
onRowsRemoved: configRoot.refreshDetectedSources()
onModelReset: configRoot.refreshDetectedSources()
onDataChanged: configRoot.refreshDetectedSources()
Component.onCompleted: configRoot.refreshDetectedSources()
}
component SectionHeader: Item {
required property string title
+7
View File
@@ -43,6 +43,10 @@ PlasmoidItem {
// qmllint disable missing-property
property bool configuredShowMediaControls: plasmoid.configuration.showMediaControls
// qmllint disable missing-property
property string configuredSourcePriority: plasmoid.configuration.sourcePriority
// qmllint disable missing-property
property bool configuredSourceWhitelist: plasmoid.configuration.sourceWhitelist
// qmllint disable missing-property
property int configuredTrackTextVerticalSpacing: plasmoid.configuration.trackTextVerticalSpacing
// qmllint disable missing-property
property int configuredLabelVerticalSpacing: plasmoid.configuration.labelVerticalSpacing
@@ -73,6 +77,9 @@ PlasmoidItem {
Player {
id: player
sourcePriority: root.configuredSourcePriority
sourceWhitelist: root.configuredSourceWhitelist
}
fullRepresentation: Representation {