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.
391 lines
15 KiB
391 lines
15 KiB
/*************************************************************************** |
|
* Copyright 2013 Sebastian Kügler <sebas@kde.org> * |
|
* Copyright 2014, 2016 Kai Uwe Broulik <kde@privat.broulik.de> * |
|
* * |
|
* This program is free software; you can redistribute it and/or modify * |
|
* it under the terms of the GNU Library General Public License as * |
|
* published by the Free Software Foundation; either version 2 of the * |
|
* License, or (at your option) any later version. * |
|
* * |
|
* This program is distributed in the hope that it will be useful, * |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of * |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
|
* GNU Library General Public License for more details. * |
|
* * |
|
* You should have received a copy of the GNU Library General Public * |
|
* License along with this program; if not, write to the * |
|
* Free Software Foundation, Inc., * |
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * |
|
***************************************************************************/ |
|
|
|
import QtQuick 2.4 |
|
import QtQuick.Layouts 1.1 |
|
import org.kde.plasma.core 2.0 as PlasmaCore |
|
import org.kde.plasma.components 2.0 as PlasmaComponents |
|
import org.kde.plasma.extras 2.0 as PlasmaExtras |
|
import org.kde.kcoreaddons 1.0 as KCoreAddons |
|
|
|
Item { |
|
id: expandedRepresentation |
|
|
|
Layout.minimumWidth: Layout.minimumHeight * 1.333 |
|
Layout.minimumHeight: units.gridUnit * 10 |
|
Layout.preferredWidth: Layout.minimumWidth * 1.5 |
|
Layout.preferredHeight: Layout.minimumHeight * 1.5 |
|
|
|
readonly property int controlSize: Math.min(height, width) / 4 |
|
|
|
property int position: mpris2Source.currentData.Position || 0 |
|
readonly property real rate: mpris2Source.currentData.Rate || 1 |
|
readonly property int length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 |
|
|
|
property bool disablePositionUpdate: false |
|
property bool keyPressed: false |
|
|
|
function retrievePosition() { |
|
var service = mpris2Source.serviceForSource(mpris2Source.current); |
|
var operation = service.operationDescription("GetPosition"); |
|
service.startOperationCall(operation); |
|
} |
|
|
|
Connections { |
|
target: plasmoid |
|
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 |
|
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.maximumValue = length |
|
retrievePosition() |
|
disablePositionUpdate = false |
|
} |
|
|
|
Keys.onPressed: keyPressed = true |
|
|
|
Keys.onReleased: { |
|
keyPressed = 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.action_playPause() |
|
} 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_Left || event.key === Qt.Key_J) { // TODO ltr languages |
|
// seek back 5s |
|
seekSlider.value = Math.max(0, seekSlider.value - 5000000) // microseconds |
|
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { |
|
// seek forward 5s |
|
seekSlider.value = Math.min(seekSlider.maximumValue, seekSlider.value + 5000000) |
|
} else if (event.key === Qt.Key_Home) { |
|
seekSlider.value = 0 |
|
} else if (event.key === Qt.Key_End) { |
|
seekSlider.value = seekSlider.maximumValue |
|
} 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.maximumValue * (event.key - Qt.Key_0) / 10 |
|
} else { |
|
event.accepted = false |
|
} |
|
} |
|
} |
|
|
|
ColumnLayout { |
|
id: titleColumn |
|
width: parent.width |
|
spacing: units.smallSpacing |
|
|
|
PlasmaComponents.ComboBox { |
|
id: playerCombo |
|
Layout.fillWidth: true |
|
visible: model.length > 2 // more than one player, @multiplex is always there |
|
model: { |
|
var model = [{ |
|
text: i18n("Choose player automatically"), |
|
source: mpris2Source.multiplexSource |
|
}] |
|
|
|
var sources = mpris2Source.sources |
|
for (var i = 0, length = sources.length; i < length; ++i) { |
|
var source = sources[i] |
|
if (source === mpris2Source.multiplexSource) { |
|
continue |
|
} |
|
|
|
// we could show the pretty player name ("Identity") here but then we |
|
// would have to connect all sources just for this |
|
model.push({text: source, source: source}) |
|
} |
|
|
|
return model |
|
} |
|
|
|
onModelChanged: { |
|
// if model changes, ComboBox resets, so we try to find the current player again... |
|
for (var i = 0, length = model.length; i < length; ++i) { |
|
if (model[i].source === mpris2Source.current) { |
|
currentIndex = i |
|
break |
|
} |
|
} |
|
} |
|
|
|
onActivated: { |
|
disablePositionUpdate = true |
|
// ComboBox has currentIndex and currentText, why doesn't it have currentItem/currentModelValue? |
|
mpris2Source.current = model[index].source |
|
disablePositionUpdate = false |
|
} |
|
} |
|
|
|
RowLayout { |
|
id: titleRow |
|
Layout.fillWidth: true |
|
Layout.minimumHeight: albumArt.Layout.preferredHeight |
|
spacing: units.largeSpacing |
|
|
|
Image { |
|
id: albumArt |
|
readonly property int size: Math.round(expandedRepresentation.height / 2 - (playerCombo.count > 2 ? playerCombo.height : 0)) |
|
source: root.albumArt |
|
asynchronous: true |
|
fillMode: Image.PreserveAspectCrop |
|
sourceSize: Qt.size(size, size) |
|
Layout.preferredHeight: size |
|
Layout.preferredWidth: size |
|
visible: !!root.track && status === Image.Ready |
|
} |
|
|
|
ColumnLayout { |
|
Layout.fillWidth: true |
|
spacing: units.smallSpacing / 2 |
|
|
|
PlasmaExtras.Heading { |
|
id: song |
|
Layout.fillWidth: true |
|
level: 3 |
|
opacity: 0.6 |
|
|
|
maximumLineCount: 3 |
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere |
|
elide: Text.ElideRight |
|
text: root.track ? root.track : i18n("No media playing") |
|
} |
|
|
|
PlasmaExtras.Heading { |
|
id: artist |
|
Layout.fillWidth: true |
|
level: 4 |
|
opacity: 0.4 |
|
maximumLineCount: 2 |
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere |
|
visible: text !== "" |
|
|
|
elide: Text.ElideRight |
|
text: root.artist || "" |
|
} |
|
|
|
PlasmaExtras.Heading { |
|
Layout.fillWidth: true |
|
level: 5 |
|
opacity: 0.4 |
|
wrapMode: Text.NoWrap |
|
elide: Text.ElideRight |
|
visible: text !== "" |
|
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 "" |
|
} |
|
} |
|
} |
|
} |
|
|
|
RowLayout { |
|
Layout.fillWidth: true |
|
spacing: units.smallSpacing |
|
|
|
// if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case |
|
enabled: !root.noPlayer && root.track && seekSlider.maximumValue > 0 && mpris2Source.currentData.CanSeek ? true : false |
|
opacity: enabled ? 1 : 0 |
|
Behavior on opacity { |
|
NumberAnimation { duration: units.longDuration } |
|
} |
|
|
|
// ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song |
|
TextMetrics { |
|
id: timeMetrics |
|
text: i18nc("Remaining time for song e.g -5:42", "-%1", |
|
KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, KCoreAddons.FormatTypes.FoldHours)) |
|
font: theme.smallestFont |
|
} |
|
|
|
PlasmaComponents.Label { |
|
Layout.preferredWidth: timeMetrics.width |
|
Layout.fillHeight: true |
|
verticalAlignment: Text.AlignVCenter |
|
horizontalAlignment: Text.AlignRight |
|
text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, KCoreAddons.FormatTypes.FoldHours) |
|
opacity: 0.6 |
|
font: theme.smallestFont |
|
} |
|
|
|
PlasmaComponents.Slider { |
|
id: seekSlider |
|
Layout.fillWidth: true |
|
z: 999 |
|
value: 0 |
|
|
|
onValueChanged: { |
|
if (!disablePositionUpdate) { |
|
// delay setting the position to avoid race conditions |
|
queuedPositionUpdate.restart() |
|
} |
|
} |
|
|
|
Timer { |
|
id: seekTimer |
|
interval: 1000 / expandedRepresentation.rate |
|
repeat: true |
|
running: root.state == "playing" && plasmoid.expanded && !keyPressed && interval > 0 |
|
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.maximumValue) { |
|
retrievePosition(); |
|
} else { |
|
seekSlider.value += 1000000 |
|
} |
|
disablePositionUpdate = false |
|
} |
|
} |
|
} |
|
} |
|
|
|
PlasmaComponents.Label { |
|
Layout.preferredWidth: timeMetrics.width |
|
Layout.fillHeight: true |
|
verticalAlignment: Text.AlignVCenter |
|
text: i18nc("Remaining time for song e.g -5:42", "-%1", |
|
KCoreAddons.Format.formatDuration((seekSlider.maximumValue - seekSlider.value) / 1000, KCoreAddons.FormatTypes.FoldHours)) |
|
opacity: 0.6 |
|
font: theme.smallestFont |
|
} |
|
} |
|
} |
|
|
|
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) |
|
} |
|
} |
|
|
|
Item { |
|
anchors.bottom: parent.bottom |
|
width: parent.width |
|
height: playerControls.height |
|
|
|
Row { |
|
id: playerControls |
|
property bool enabled: root.canControl |
|
property int controlsSize: theme.mSize(theme.defaultFont).height * 3 |
|
|
|
anchors.horizontalCenter: parent.horizontalCenter |
|
spacing: units.largeSpacing |
|
|
|
PlasmaComponents.ToolButton { |
|
anchors.verticalCenter: parent.verticalCenter |
|
width: expandedRepresentation.controlSize |
|
height: width |
|
enabled: playerControls.enabled && root.canGoPrevious |
|
iconSource: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" |
|
onClicked: { |
|
seekSlider.value = 0 // Let the media start from beginning. Bug 362473 |
|
root.action_previous() |
|
} |
|
} |
|
|
|
PlasmaComponents.ToolButton { |
|
width: Math.round(expandedRepresentation.controlSize * 1.5) |
|
height: width |
|
enabled: playerControls.enabled |
|
iconSource: root.state == "playing" ? "media-playback-pause" : "media-playback-start" |
|
onClicked: root.action_playPause() |
|
} |
|
|
|
PlasmaComponents.ToolButton { |
|
anchors.verticalCenter: parent.verticalCenter |
|
width: expandedRepresentation.controlSize |
|
height: width |
|
enabled: playerControls.enabled && root.canGoNext |
|
iconSource: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" |
|
onClicked: { |
|
seekSlider.value = 0 // Let the media start from beginning. Bug 362473 |
|
root.action_next() |
|
} |
|
} |
|
} |
|
} |
|
}
|
|
|