You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

839 lines
36 KiB

/*
SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2014, 2016 Kai Uwe Broulik <kde@privat.broulik.de>
SPDX-FileCopyrightText: 2020 Carson Black <uhhadd@gmail.com>
SPDX-FileCopyrightText: 2020 Ismael Asensio <isma.af@gmail.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.1
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.kcoreaddons 1.0 as KCoreAddons
import org.kde.kirigami 2.4 as Kirigami
import QtGraphicalEffects 1.0
PlasmaExtras.Representation {
id: expandedRepresentation
Layout.minimumWidth: PlasmaCore.Units.gridUnit * 14
Layout.minimumHeight: PlasmaCore.Units.gridUnit * 14
Layout.preferredWidth: PlasmaCore.Units.gridUnit * 20
Layout.preferredHeight: PlasmaCore.Units.gridUnit * 20
Layout.maximumWidth: PlasmaCore.Units.gridUnit * 40
Layout.maximumHeight: PlasmaCore.Units.gridUnit * 40
collapseMarginsHint: true
readonly property int controlSize: PlasmaCore.Units.iconSizes.medium
property double position: (mpris2Source.currentData && mpris2Source.currentData.Position) || 0
readonly property real rate: (mpris2Source.currentData && mpris2Source.currentData.Rate) || 1
readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0
readonly property bool canSeek: (mpris2Source.currentData && mpris2Source.currentData.CanSeek) || false
readonly property bool softwareRendering: GraphicsInfo.api === GraphicsInfo.Software
readonly property var appletInterface: Plasmoid.self
// only show hours (the default for KFormat) when track is actually longer than an hour
readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours
property bool disablePositionUpdate: false
property bool keyPressed: false
KeyNavigation.down: playerSelector.count ? playerSelector.currentItem : (seekSlider.visible ? seekSlider : seekSlider.KeyNavigation.down)
KeyNavigation.up: seekSlider.KeyNavigation.down
function retrievePosition() {
var service = mpris2Source.serviceForSource(mpris2Source.current);
var operation = service.operationDescription("GetPosition");
service.startOperationCall(operation);
}
Connections {
target: Plasmoid.self
function onExpandedChanged() {
if (Plasmoid.expanded) {
retrievePosition();
}
}
}
onPositionChanged: {
// we don't want to interrupt the user dragging the slider
if (!seekSlider.pressed && !keyPressed) {
// we also don't want passive position updates
disablePositionUpdate = true
// Slider refuses to set value beyond its end, make sure "to" is up-to-date first
seekSlider.to = length;
seekSlider.value = position
disablePositionUpdate = false
}
}
onLengthChanged: {
disablePositionUpdate = true
// When reducing maximumValue, value is clamped to it, however
// when increasing it again it gets its old value back.
// To keep us from seeking to the end of the track when moving
// to a new track, we'll reset the value to zero and ask for the position again
seekSlider.value = 0
seekSlider.to = length
retrievePosition()
disablePositionUpdate = false
}
Keys.onPressed: keyPressed = true
Keys.onReleased: {
keyPressed = false
if ((event.key == Qt.Key_Tab || event.key == Qt.Key_Backtab) && event.modifiers & Qt.ControlModifier) {
event.accepted = true;
if (root.mprisSourcesModel.length > 2) {
var nextIndex = playerSelector.currentIndex + 1;
if (event.key == Qt.Key_Backtab || event.modifiers & Qt.ShiftModifier) {
nextIndex -= 2;
}
if (nextIndex == root.mprisSourcesModel.length) {
nextIndex = 0;
}
if (nextIndex < 0) {
nextIndex = root.mprisSourcesModel.length - 1;
}
playerSelector.currentIndex = nextIndex;
disablePositionUpdate = true;
mpris2Source.current = root.mprisSourcesModel[nextIndex]["source"];
disablePositionUpdate = false;
}
}
if (!event.modifiers) {
event.accepted = true
if (event.key === Qt.Key_Space || event.key === Qt.Key_K) {
// K is YouTube's key for "play/pause" :)
root.togglePlaying()
} else if (event.key === Qt.Key_P) {
root.action_previous()
} else if (event.key === Qt.Key_N) {
root.action_next()
} else if (event.key === Qt.Key_S) {
root.action_stop()
} else if (event.key === Qt.Key_J) { // TODO ltr languages
// seek back 5s
seekSlider.value = Math.max(0, seekSlider.value - 5000000) // microseconds
seekSlider.moved();
} else if (event.key === Qt.Key_L) {
// seek forward 5s
seekSlider.value = Math.min(seekSlider.to, seekSlider.value + 5000000)
seekSlider.moved();
} else if (event.key === Qt.Key_Home) {
seekSlider.value = 0
seekSlider.moved();
} else if (event.key === Qt.Key_End) {
seekSlider.value = seekSlider.to
seekSlider.moved();
} else if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
// jump to percentage, ie. 0 = beginnign, 1 = 10% of total length etc
seekSlider.value = seekSlider.to * (event.key - Qt.Key_0) / 10
seekSlider.moved();
} else {
event.accepted = false
}
}
}
Item { // Album Art Background + Details
anchors.fill: parent
clip: true
ShaderEffect {
id: backgroundImage
property real scaleFactor: 1.0
property ShaderEffectSource source: ShaderEffectSource {
id: shaderEffectSource
sourceItem: albumArt
}
anchors.centerIn: parent
visible: (exitTransition.running || popExitTransition.running || albumArt.hasImage) && !softwareRendering
layer.enabled: !softwareRendering
layer.effect: HueSaturation {
cached: true
lightness: -0.5
saturation: 0.9
layer.enabled: true
layer.effect: GaussianBlur {
cached: true
radius: 128
deviation: 12
samples: 63
transparentBorder: false
}
}
// use State to avoid unnecessary reevaluation of width and height
states: State {
name: "albumArtReady"
when: Plasmoid.expanded && backgroundImage.visible && albumArt.currentItem.paintedWidth > 0
PropertyChanges {
target: backgroundImage
scaleFactor: Math.max(parent.width / albumArt.currentItem.paintedWidth, parent.height / albumArt.currentItem.paintedHeight)
width: Math.round(albumArt.currentItem.paintedWidth * scaleFactor)
height: Math.round(albumArt.currentItem.paintedHeight * scaleFactor)
}
PropertyChanges {
target: shaderEffectSource
// HACK: Fix background ratio when DPI > 1
sourceRect: Qt.rect(albumArt.width - albumArt.currentItem.paintedWidth,
Math.round((albumArt.height - albumArt.currentItem.paintedHeight) / 2),
albumArt.currentItem.paintedWidth,
albumArt.currentItem.paintedHeight)
}
}
}
RowLayout { // Album Art + Details
id: albumRow
anchors {
fill: parent
leftMargin: PlasmaCore.Units.largeSpacing
rightMargin: PlasmaCore.Units.largeSpacing
}
spacing: PlasmaCore.Units.largeSpacing
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 50
QQC2.StackView {
id: albumArt
anchors.fill: parent
readonly property bool hasImage: currentItem instanceof Image
&& (currentItem.status === Image.Ready || currentItem.status === Image.Loading)
replaceEnter: Transition {
OpacityAnimator {
from: 0
to: 1
duration: PlasmaCore.Units.longDuration
}
}
replaceExit: Transition {
id: exitTransition
SequentialAnimation {
PauseAnimation {
duration: PlasmaCore.Units.longDuration
}
/**
* If the new ratio and the old ratio are different,
* perform a fade-out animation for the old image
* to prevent it from suddenly disappearing.
*/
OpacityAnimator {
id: exitTransitionOpacityAnimator
from: 1
to: 0
duration: 0
}
}
}
popExit: Transition {
id: popExitTransition
OpacityAnimator {
from: 1
to: 0
duration: PlasmaCore.Units.longDuration
}
}
Connections {
enabled: Plasmoid.expanded
target: root
function onAlbumArtChanged() {
albumArt.loadAlbumArt();
}
}
Connections {
target: plasmoid
function onExpandedChanged() {
// NOTE: Don't use strict equality
if (!Plasmoid.expanded || (albumArt.currentItem instanceof Image && albumArt.currentItem.source == root.albumArt)) {
return;
}
albumArt.loadAlbumArt();
}
}
function loadAlbumArt() {
if (!root.albumArt) {
albumArt.clear(QQC2.StackView.PopTransition);
return;
}
const oldImageRatio = albumArt.currentItem instanceof Image ? albumArt.currentItem.sourceSize.width / albumArt.currentItem.sourceSize.height : 1;
const pendingImage = albumArtComponent.createObject(albumArt, {
"source": root.albumArt,
"opacity": 0,
});
function replaceWhenLoaded() {
if (pendingImage.status === Image.Loading) {
return;
}
if (pendingImage.status === Image.Null || pendingImage.status === Image.Error) {
pendingImage.destroy();
// Also clear the old image
albumArt.clear(QQC2.StackView.PopTransition);
return;
}
const newImageRatio = pendingImage.sourceSize.width / pendingImage.sourceSize.height;
exitTransitionOpacityAnimator.duration = oldImageRatio === newImageRatio ? 0 : PlasmaCore.Units.longDuration;
albumArt.replace(pendingImage, {}, QQC2.StackView.ReplaceTransition);
pendingImage.statusChanged.disconnect(replaceWhenLoaded);
}
pendingImage.statusChanged.connect(replaceWhenLoaded);
replaceWhenLoaded();
}
Component {
id: albumArtComponent
Image { // Album Art
horizontalAlignment: Image.AlignRight
verticalAlignment: Image.AlignVCenter
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: false
QQC2.StackView.onRemoved: {
source = ""; // HACK: Reduce memory usage
destroy();
}
}
}
}
Loader {
id: fallbackIconLoader
// When albumArt is shown, the icon is unloaded to reduce memory usage.
readonly property string icon: (mpris2Source.currentData && mpris2Source.currentData["Desktop Icon Name"]) || "media-album-cover"
active: Plasmoid.expanded && !albumArt.hasImage
anchors.fill: parent
sourceComponent: root.track ? fallbackIconItem : placeholderMessage
opacity: active ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: PlasmaCore.Units.longDuration
}
}
Component {
id: fallbackIconItem
PlasmaCore.IconItem { // Fallback
source: icon
anchors {
fill: parent
margins: PlasmaCore.Units.largeSpacing * 2
}
}
}
Component {
id: placeholderMessage
Item { // Put PlaceholderMessage in Item so PlaceholderMessage will not fill its parent.
anchors.fill: parent
PlasmaExtras.PlaceholderMessage { // "No media playing" placeholder message
width: parent.width // For text wrap
anchors.centerIn: parent
iconName: icon
text: i18n("No media playing")
}
}
}
}
}
ColumnLayout { // Details Column
visible: root.track
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 50
/*
* 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.hasImage) ? PlasmaCore.ColorScope.textColor : "white"
textFormat: Text.PlainText
wrapMode: Text.Wrap
fontSizeMode: Text.VerticalFit
elide: Text.ElideRight
text: root.track
Layout.fillWidth: true
Layout.maximumHeight: PlasmaCore.Units.gridUnit*5
}
Kirigami.Heading { // Song Artist
id: songArtist
visible: root.artist
level: 2
color: (softwareRendering || !albumArt.hasImage) ? PlasmaCore.ColorScope.textColor : "white"
textFormat: Text.PlainText
wrapMode: Text.Wrap
fontSizeMode: Text.VerticalFit
elide: Text.ElideRight
text: root.artist
Layout.fillWidth: true
Layout.maximumHeight: PlasmaCore.Units.gridUnit*2
}
Kirigami.Heading { // Song Album
color: (softwareRendering || !albumArt.hasImage) ? 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
}
// 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 ""
}
var urlParts = xesamUrl.split("/")
if (urlParts.length < 3) {
return ""
}
var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename
if (lastFolderPath) {
return lastFolderPath
}
return ""
}
Layout.fillWidth: true
Layout.maximumHeight: PlasmaCore.Units.gridUnit*2
}
}
}
}
footer: PlasmaExtras.PlasmoidHeading {
id: footerItem
location: PlasmaExtras.PlasmoidHeading.Location.Footer
ColumnLayout { // Main Column Layout
anchors.fill: parent
RowLayout { // Seek Bar
spacing: PlasmaCore.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: PlasmaCore.Units.longDuration }
}
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.maximumWidth: Math.min(PlasmaCore.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: PlasmaCore.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: PlasmaCore.Theme.smallestFont
color: PlasmaCore.ColorScope.textColor
}
PlasmaComponents3.Slider { // Slider
id: seekSlider
Layout.fillWidth: true
z: 999
value: 0
visible: canSeek
KeyNavigation.up: playerSelector.currentItem
KeyNavigation.down: playPauseButton.enabled ? playPauseButton : (playPauseButton.KeyNavigation.left.enabled ? playPauseButton.KeyNavigation.left : playPauseButton.KeyNavigation.right)
Keys.onLeftPressed: {
seekSlider.value = Math.max(0, seekSlider.value - 5000000) // microseconds
seekSlider.moved();
}
Keys.onRightPressed: {
seekSlider.value = Math.max(0, seekSlider.value + 5000000) // microseconds
seekSlider.moved();
}
onMoved: {
if (!disablePositionUpdate) {
// delay setting the position to avoid race conditions
queuedPositionUpdate.restart()
}
}
onPressedChanged: {
// Property binding evaluation is non-deterministic
// so binding visible to pressed and delay to 0 when pressed
// will not make the tooltip show up immediately.
if (pressed) {
seekToolTip.delay = 0;
seekToolTip.visible = true;
} else {
seekToolTip.delay = Qt.binding(() => Kirigami.Units.toolTipDelay);
seekToolTip.visible = Qt.binding(() => seekToolTipHandler.hovered);
}
}
HoverHandler {
id: seekToolTipHandler
}
PlasmaComponents3.ToolTip {
id: seekToolTip
readonly property real position: {
if (seekSlider.pressed) {
return seekSlider.visualPosition;
}
// does not need mirroring since we work on raw mouse coordinates
const mousePos = seekToolTipHandler.point.position.x - seekSlider.handle.width / 2;
return Math.max(0, Math.min(1, mousePos / (seekSlider.width - seekSlider.handle.width)));
}
x: Math.round(seekSlider.handle.width / 2 + position * (seekSlider.width - seekSlider.handle.width) - width / 2)
// Never hide (not on press, no timeout) as long as the mouse is hovered
closePolicy: PlasmaComponents3.Popup.NoAutoClose
timeout: -1
text: {
// Label text needs mirrored position again
const effectivePosition = seekSlider.mirrored ? (1 - position) : position;
return KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.from) * effectivePosition / 1000, expandedRepresentation.durationFormattingOptions)
}
// NOTE also controlled in onPressedChanged handler above
visible: seekToolTipHandler.hovered
}
Timer {
id: seekTimer
interval: 1000 / expandedRepresentation.rate
repeat: true
running: root.isPlaying && 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
}
}
}
}
RowLayout {
visible: !canSeek
Layout.fillWidth: true
Layout.preferredHeight: seekSlider.height
PlasmaComponents3.ProgressBar { // Time Remaining
value: seekSlider.value
from: seekSlider.from
to: seekSlider.to
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: PlasmaCore.Theme.smallestFont
color: PlasmaCore.ColorScope.textColor
}
}
RowLayout { // Player Controls
id: playerControls
property bool enabled: root.canControl
property int controlsSize: PlasmaCore.Theme.mSize(PlasmaCore.Theme.defaultFont).height * 3
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: PlasmaCore.Units.smallSpacing
spacing: PlasmaCore.Units.smallSpacing
PlasmaComponents3.ToolButton {
id: shuffleButton
Layout.rightMargin: LayoutMirroring.enabled ? 0 : PlasmaCore.Units.largeSpacing - playerControls.spacing
Layout.leftMargin: LayoutMirroring.enabled ? PlasmaCore.Units.largeSpacing - playerControls.spacing : 0
icon.name: "media-playlist-shuffle"
icon.width: expandedRepresentation.controlSize
icon.height: expandedRepresentation.controlSize
checked: root.shuffle === true
enabled: root.canControl && root.shuffle !== undefined
display: PlasmaComponents3.AbstractButton.IconOnly
text: i18n("Shuffle")
KeyNavigation.right: previousButton.enabled ? previousButton : previousButton.KeyNavigation.right
KeyNavigation.up: playPauseButton.KeyNavigation.up
onClicked: {
const service = mpris2Source.serviceForSource(mpris2Source.current);
let operation = service.operationDescription("SetShuffle");
operation.on = !root.shuffle;
service.startOperationCall(operation);
}
PlasmaComponents3.ToolTip {
text: parent.text
}
}
PlasmaComponents3.ToolButton { // Previous
id: previousButton
icon.width: expandedRepresentation.controlSize
icon.height: expandedRepresentation.controlSize
Layout.alignment: Qt.AlignVCenter
enabled: playerControls.enabled && root.canGoPrevious
icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
display: PlasmaComponents3.AbstractButton.IconOnly
text: i18nc("Play previous track", "Previous Track")
KeyNavigation.left: shuffleButton
KeyNavigation.right: playPauseButton.enabled ? playPauseButton : playPauseButton.KeyNavigation.right
KeyNavigation.up: playPauseButton.KeyNavigation.up
onClicked: {
seekSlider.value = 0 // Let the media start from beginning. Bug 362473
root.action_previous()
}
}
PlasmaComponents3.ToolButton { // Pause/Play
id: playPauseButton
icon.width: expandedRepresentation.controlSize
icon.height: expandedRepresentation.controlSize
Layout.alignment: Qt.AlignVCenter
enabled: root.isPlaying ? root.canPause : root.canPlay
icon.name: root.isPlaying ? "media-playback-pause" : "media-playback-start"
display: PlasmaComponents3.AbstractButton.IconOnly
text: root.isPlaying ? i18nc("Pause playback", "Pause") : i18nc("Start playback", "Play")
KeyNavigation.left: previousButton.enabled ? previousButton : previousButton.KeyNavigation.left
KeyNavigation.right: nextButton.enabled ? nextButton : nextButton.KeyNavigation.right
KeyNavigation.up: seekSlider.visible ? seekSlider : seekSlider.KeyNavigation.up
onClicked: root.togglePlaying()
}
PlasmaComponents3.ToolButton { // Next
id: nextButton
icon.width: expandedRepresentation.controlSize
icon.height: expandedRepresentation.controlSize
Layout.alignment: Qt.AlignVCenter
enabled: playerControls.enabled && root.canGoNext
icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
display: PlasmaComponents3.AbstractButton.IconOnly
text: i18nc("Play next track", "Next Track")
KeyNavigation.left: playPauseButton.enabled ? playPauseButton : playPauseButton.KeyNavigation.left
KeyNavigation.right: repeatButton
KeyNavigation.up: playPauseButton.KeyNavigation.up
onClicked: {
seekSlider.value = 0 // Let the media start from beginning. Bug 362473
root.action_next()
}
}
PlasmaComponents3.ToolButton {
id: repeatButton
Layout.leftMargin: LayoutMirroring.enabled ? 0 : PlasmaCore.Units.largeSpacing - playerControls.spacing
Layout.rightMargin: LayoutMirroring.enabled ? PlasmaCore.Units.largeSpacing - playerControls.spacing : 0
icon.name: root.loopStatus === "Track" ? "media-playlist-repeat-song" : "media-playlist-repeat"
icon.width: expandedRepresentation.controlSize
icon.height: expandedRepresentation.controlSize
checked: root.loopStatus !== undefined && root.loopStatus !== "None"
enabled: root.canControl && root.loopStatus !== undefined
display: PlasmaComponents3.AbstractButton.IconOnly
text: root.loopStatus === "Track" ? i18n("Repeat Track") : i18n("Repeat")
KeyNavigation.left: nextButton.enabled ? nextButton : nextButton.KeyNavigation.left
KeyNavigation.up: playPauseButton.KeyNavigation.up
onClicked: {
const service = mpris2Source.serviceForSource(mpris2Source.current);
let operation = service.operationDescription("SetLoopStatus");
switch (root.loopStatus) {
case "Playlist":
operation.status = "Track";
break;
case "Track":
operation.status = "None";
break;
default:
operation.status = "Playlist";
}
service.startOperationCall(operation);
}
PlasmaComponents3.ToolTip {
text: parent.text
}
}
}
}
}
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
anchors.fill: parent
implicitHeight: contentHeight
onCurrentIndexChanged: {
disablePositionUpdate = true;
mpris2Source.current = playerList.model[currentIndex]["source"];
disablePositionUpdate = false;
}
Repeater {
id: playerList
model: root.mprisSourcesModel
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"]
}
// Keep the delegate centered by offsetting the padding removed in the parent
bottomPadding: verticalPadding + headerItem.bottomPadding
topPadding: verticalPadding - headerItem.bottomPadding
KeyNavigation.down: seekSlider.visible ? seekSlider : seekSlider.KeyNavigation.down
}
onModelChanged: {
playerSelector.currentIndex = model.findIndex(
(data) => { return data.source === mpris2Source.current }
)
}
}
}
}
Timer {
id: queuedPositionUpdate
interval: 100
onTriggered: {
if (position == seekSlider.value) {
return;
}
var service = mpris2Source.serviceForSource(mpris2Source.current)
var operation = service.operationDescription("SetPosition")
operation.microseconds = seekSlider.value
service.startOperationCall(operation)
}
}
}