diff --git a/applets/mediacontroller/contents/config/config.qml b/applets/mediacontroller/contents/config/config.qml
new file mode 100644
--- /dev/null
+++ b/applets/mediacontroller/contents/config/config.qml
@@ -0,0 +1,11 @@
+import QtQuick 2.4
+
+import org.kde.plasma.configuration 2.0
+
+ConfigModel {
+ ConfigCategory {
+ name: i18n("General")
+ icon: "preferences-desktop-plasma"
+ source: "configView.qml"
+ }
+}
\ No newline at end of file
diff --git a/applets/mediacontroller/contents/config/main.xml b/applets/mediacontroller/contents/config/main.xml
new file mode 100644
--- /dev/null
+++ b/applets/mediacontroller/contents/config/main.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ false
+
+
+
+
\ No newline at end of file
diff --git a/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml b/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml
--- a/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml
+++ b/applets/mediacontroller/contents/ui/ExpandedRepresentation.qml
@@ -1,6 +1,8 @@
/***************************************************************************
* Copyright 2013 Sebastian Kügler *
* Copyright 2014, 2016 Kai Uwe Broulik *
+ * Copyright 2020 Carson Black *
+ * Copyright 2020 Ismael Asensio *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Library General Public License as *
@@ -18,27 +20,31 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
***************************************************************************/
-import QtQuick 2.4
+import QtQuick 2.8
import QtQuick.Layouts 1.1
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 QtGraphicalEffects 1.0
Item {
id: expandedRepresentation
- Layout.minimumWidth: Layout.minimumHeight * 1.333
- Layout.minimumHeight: units.gridUnit * 10
+ Layout.minimumWidth: units.gridUnit * 15
+ Layout.minimumHeight: units.gridUnit * 23
Layout.preferredWidth: Layout.minimumWidth * 1.5
Layout.preferredHeight: Layout.minimumHeight * 1.5
+ Layout.margins: units.largeSpacing
+
readonly property int controlSize: units.iconSizes.large
property double position: mpris2Source.currentData.Position || 0
readonly property real rate: mpris2Source.currentData.Rate || 1
readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0
readonly property bool canSeek: mpris2Source.currentData.CanSeek || false
+ readonly property bool softwareRendering: GraphicsInfo.api === GraphicsInfo.Software
// 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
@@ -124,106 +130,185 @@
}
}
- PlasmaComponents3.ComboBox {
- id: playerCombo
- width: Math.round(0.6 * parent.width)
- height: visible ? undefined : 0
- anchors.horizontalCenter: parent.horizontalCenter
- textRole: "text"
- 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
+ ColumnLayout { // Main Column Layout
+ id: mainCol
+ anchors.fill: parent
+
+ Item { // Album Art Background + Details
+ Layout.margins: units.smallSpacing
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ Image {
+ id: backgroundImage
+
+ source: root.albumArt
+ sourceSize.width: 512 /* Setting a source size means the item doesn't need
+ to recompute blur as the user resizes the plasmoid
+ Additionally, it puts a bit of a cap on how large the
+ buffer getting blurred can be, saving resources.
+ */
+
+ anchors.fill: parent
+ anchors.margins: -units.smallSpacing*2
+ fillMode: Image.PreserveAspectCrop
+
+ asynchronous: true
+ visible: !!root.track && status === Image.Ready && !softwareRendering
+
+ layer.enabled: !softwareRendering
+ layer.effect: HueSaturation {
+ cached: true
+
+ lightness: -0.5
+ saturation: 0.9
+
+ layer.enabled: true
+ layer.effect: GaussianBlur {
+ cached: true
+
+ radius: 256
+ deviation: 12
+ samples: 129
+
+ transparentBorder: false
+ }
}
-
- // 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})
}
+ RowLayout { // Album Art + Details
+ id: albumRow
+ spacing: units.largeSpacing
- return model
- }
+ anchors.fill: parent
+
+ Item {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.preferredWidth: parent.width / 2
+ Layout.maximumWidth: parent.width / 2
+
+ visible: albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"]
+
+ Image { // Album Art
+ id: albumArt
+
+ anchors {
+ fill: parent
+ margins: units.smallSpacing
+ }
- 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
+ visible: !!root.track && status === Image.Ready
+
+ asynchronous: true
+
+ horizontalAlignment: Image.AlignRight
+ verticalAlignment: Image.AlignVCenter
+ fillMode: Image.PreserveAspectFit
+
+ source: root.albumArt
+ }
+
+ PlasmaCore.IconItem { // Fallback
+ visible: !albumArt.visible && !!mpris2Source.currentData["Desktop Icon Name"]
+ source: mpris2Source.currentData["Desktop Icon Name"]
+
+ anchors {
+ fill: parent
+ margins: units.smallSpacing
+ }
+
+
+ }
}
- }
- }
- onActivated: {
- disablePositionUpdate = true
- // ComboBox has currentIndex and currentText, why doesn't it have currentItem/currentModelValue?
- mpris2Source.current = model[index].source
- disablePositionUpdate = false
- }
- }
+ ColumnLayout { // Details Column
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.preferredWidth: parent.width / 2
+ Layout.maximumWidth: parent.width / 2
+ Layout.alignment: !(albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"]) ? Qt.AlignHCenter : 0
- Item {
- anchors {
- horizontalCenter: parent.horizontalCenter
- top: playerCombo.bottom
- bottom: controlCol.top
- margins: units.smallSpacing
- }
+ PlasmaExtras.Heading { // Song Title
+ id: songTitle
+ level: 1
+
+ color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white"
- PlasmaCore.IconItem {
- anchors {
- horizontalCenter: parent.horizontalCenter
- verticalCenter: parent.verticalCenter
- }
+ textFormat: Text.PlainText
+ wrapMode: Text.Wrap
+ fontSizeMode: Text.VerticalFit
- height: Math.round(parent.height / 2)
- width: height
+ text: root.track || i18n("No media playing")
- source: mpris2Source.currentData["Desktop Icon Name"]
- visible: !albumArt.visible
+ Layout.maximumWidth: parent.width
+ Layout.maximumHeight: units.gridUnit*5
+ }
+ PlasmaExtras.Heading { // Song Artist
+ id: songArtist
+ visible: root.track && root.artist
+ level: 2
- usesPlasmaTheme: false
- }
- }
+ color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white"
- Image {
- id: albumArt
- anchors {
- left: parent.left
- right: parent.right
- top: playerCombo.bottom
- bottom: controlCol.top
- margins: units.smallSpacing
- }
- source: root.albumArt
- asynchronous: true
- fillMode: Image.PreserveAspectFit
- sourceSize: Qt.size(height, height)
- visible: !!root.track && status === Image.Ready
- }
+ textFormat: Text.PlainText
+ wrapMode: Text.Wrap
+ fontSizeMode: Text.VerticalFit
- Column {
- id: controlCol
- width: parent.width
- anchors.bottom: parent.bottom
+ text: root.artist
+ Layout.maximumWidth: parent.width
+ Layout.maximumHeight: units.gridUnit*2
+ }
+ PlasmaExtras.Heading { // Song Album
+ color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white"
+
+ level: 3
+ opacity: 0.6
+
+ textFormat: Text.PlainText
+ wrapMode: Text.Wrap
+ fontSizeMode: Text.VerticalFit
+
+ visible: text.length !== 0
+ text: {
+ var metadata = root.currentMetadata
+ if (!metadata) {
+ return ""
+ }
+ var xesamAlbum = metadata["xesam:album"]
+ if (xesamAlbum) {
+ return xesamAlbum
+ }
- spacing: units.smallSpacing
+ // if we play a local file without title and artist, show its containing folder instead
+ if (metadata["xesam:title"] || root.artist) {
+ return ""
+ }
- RowLayout {
- anchors {
- left: parent.left
- right: parent.right
- margins: units.smallSpacing
+ 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.maximumWidth: parent.width
+ Layout.maximumHeight: units.gridUnit*2
+ }
+ }
}
+ }
+ RowLayout { // Seek Bar
spacing: units.smallSpacing
// if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case
@@ -233,24 +318,29 @@
NumberAnimation { duration: units.longDuration }
}
+ Layout.alignment: Qt.AlignHCenter
+ Layout.fillWidth: true
+ Layout.maximumWidth: Math.min(units.largeSpacing*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: theme.smallestFont
}
- PlasmaComponents3.Label {
+ 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: theme.smallestFont
+ color: PlasmaCore.ColorScope.textColor
}
- PlasmaComponents3.Slider {
+ PlasmaComponents3.Slider { // Slider
id: seekSlider
Layout.fillWidth: true
z: 999
@@ -285,8 +375,9 @@
}
}
- PlasmaComponents3.ProgressBar {
+ PlasmaComponents3.ProgressBar { // Time Remaining
Layout.fillWidth: true
+ Layout.preferredHeight: seekSlider.height
value: seekSlider.value
from: seekSlider.from
to: seekSlider.to
@@ -301,116 +392,77 @@
KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions))
opacity: 0.9
font: theme.smallestFont
+ color: PlasmaCore.ColorScope.textColor
}
}
- Column {
- width: parent.width
-
- PlasmaExtras.Heading {
- id: song
- width: parent.width
- height: undefined
- level: 4
- horizontalAlignment: Text.AlignHCenter
-
- maximumLineCount: 1
- elide: Text.ElideRight
- text: {
- if (!root.track) {
- return i18n("No media playing")
- }
- return root.artist ? i18nc("artist – track", "%1 – %2", root.artist, root.track) : root.track
- }
- textFormat: Text.PlainText
- }
-
- PlasmaExtras.Heading {
- width: parent.width
- height: undefined
- level: 5
- opacity: 0.6
- horizontalAlignment: Text.AlignHCenter
- 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
- }
+ Row { // Player Controls
+ id: playerControls
- // if we play a local file without title and artist, show its containing folder instead
- if (metadata["xesam:title"] || root.artist) {
- return ""
- }
+ property bool enabled: root.canControl
+ property int controlsSize: theme.mSize(theme.defaultFont).height * 3
- var xesamUrl = (metadata["xesam:url"] || "").toString()
- if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()"
- return ""
- }
+ Layout.alignment: Qt.AlignHCenter
+ spacing: units.largeSpacing
- var urlParts = xesamUrl.split("/")
- if (urlParts.length < 3) {
- return ""
- }
+ PlasmaComponents3.ToolButton { // Previous
+ anchors.verticalCenter: parent.verticalCenter
+ width: expandedRepresentation.controlSize
+ height: width
+ enabled: playerControls.enabled && root.canGoPrevious
+ icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
+ onClicked: {
+ seekSlider.value = 0 // Let the media start from beginning. Bug 362473
+ root.action_previous()
+ }
+ }
- var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename
- if (lastFolderPath) {
- return lastFolderPath
- }
+ PlasmaComponents3.ToolButton { // Pause/Play
+ width: Math.round(expandedRepresentation.controlSize * 1.5)
+ height: width
+ enabled: root.state == "playing" ? root.canPause : root.canPlay
+ icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start"
+ onClicked: root.togglePlaying()
+ }
- return ""
+ PlasmaComponents3.ToolButton { // Next
+ anchors.verticalCenter: parent.verticalCenter
+ width: expandedRepresentation.controlSize
+ height: width
+ enabled: playerControls.enabled && root.canGoNext
+ icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
+ onClicked: {
+ seekSlider.value = 0 // Let the media start from beginning. Bug 362473
+ root.action_next()
}
- textFormat: Text.PlainText
}
}
- Item {
- width: parent.width
- height: playerControls.height
+ PlasmaComponents3.ToolButton {
+ id: playerSelector
- Row {
- id: playerControls
- property bool enabled: root.canControl
- property int controlsSize: theme.mSize(theme.defaultFont).height * 3
+ visible: tabButtonInstantiator.model.length > 2 && plasmoid.configuration.allowChangingSources // more than one player, @multiplex is always there
- anchors.horizontalCenter: parent.horizontalCenter
- spacing: units.largeSpacing
+ onClicked: menu.open()
- PlasmaComponents3.ToolButton {
- anchors.verticalCenter: parent.verticalCenter
- width: expandedRepresentation.controlSize
- height: width
- enabled: playerControls.enabled && root.canGoPrevious
- icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward"
- onClicked: {
- seekSlider.value = 0 // Let the media start from beginning. Bug 362473
- root.action_previous()
- }
- }
+ text: mpris2Source.current === mpris2Source.multiplexSource ? i18n("Choose player automatically") : mpris2Source.currentData["Identity"]
- PlasmaComponents3.ToolButton {
- width: Math.round(expandedRepresentation.controlSize * 1.5)
- height: width
- enabled: root.state == "playing" ? root.canPause : root.canPlay
- icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start"
- onClicked: root.togglePlaying()
- }
+ PlasmaComponents3.Menu {
+ id: menu
+ }
- PlasmaComponents3.ToolButton {
- anchors.verticalCenter: parent.verticalCenter
- width: expandedRepresentation.controlSize
- height: width
- enabled: playerControls.enabled && root.canGoNext
- icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward"
- onClicked: {
- seekSlider.value = 0 // Let the media start from beginning. Bug 362473
- root.action_next()
+ Instantiator {
+ id: tabButtonInstantiator
+
+ model: mprisSourcesModel
+
+ onObjectAdded: { menu.insertItem(index, object) }
+ onObjectRemoved: { menu.removeItem(object) }
+
+ delegate: PlasmaComponents3.MenuItem {
+ text: modelData["text"]
+ onTriggered: {
+ mpris2Source.current = modelData["source"]
}
}
}
@@ -421,6 +473,7 @@
id: queuedPositionUpdate
interval: 100
onTriggered: {
+ print(plasmoid.configuration)
if (position == seekSlider.value) {
return;
}
diff --git a/applets/mediacontroller/contents/ui/configView.qml b/applets/mediacontroller/contents/ui/configView.qml
new file mode 100644
--- /dev/null
+++ b/applets/mediacontroller/contents/ui/configView.qml
@@ -0,0 +1,27 @@
+import QtQuick 2.5
+import QtQuick.Controls 2.7
+
+import org.kde.kirigami 2.3 as Kirigami
+
+Item {
+ id: viewConfig
+
+ property alias cfg_allowChangingSources: allowOverride.checked
+
+ ButtonGroup {
+ buttons: formLayout.children
+ }
+
+ Kirigami.FormLayout {
+ id: formLayout
+ RadioButton {
+ id: automatic
+ text: i18n("Automatically show current media player")
+ checked: !cfg_allowChangingSources
+ }
+ RadioButton {
+ id: allowOverride
+ text: i18n("Allow me to override which media player is shown")
+ }
+ }
+}
\ No newline at end of file
diff --git a/applets/mediacontroller/contents/ui/main.qml b/applets/mediacontroller/contents/ui/main.qml
--- a/applets/mediacontroller/contents/ui/main.qml
+++ b/applets/mediacontroller/contents/ui/main.qml
@@ -1,6 +1,7 @@
/***************************************************************************
* Copyright 2013 Sebastian Kügler *
* Copyright 2014 Kai Uwe Broulik *
+ * Copyright 2020 Ismael Asensio *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Library General Public License as *
@@ -70,6 +71,8 @@
property bool noPlayer: mpris2Source.sources.length <= 1
+ property var mprisSourcesModel: []
+
readonly property bool canControl: (!root.noPlayer && mpris2Source.currentData.CanControl) || false
readonly property bool canGoPrevious: (canControl && mpris2Source.currentData.CanGoPrevious) || false
readonly property bool canGoNext: (canControl && mpris2Source.currentData.CanGoNext) || false
@@ -205,18 +208,24 @@
readonly property var currentData: data[current]
engine: "mpris2"
- connectedSources: current
+ connectedSources: sources
+
+ onSourceAdded: {
+ updateMprisSourcesModel()
+ }
onSourceRemoved: {
// if player is closed, reset to multiplex source
if (source === current) {
current = multiplexSource
}
+ updateMprisSourcesModel()
}
}
Component.onCompleted: {
- mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts();
+ mpris2Source.serviceForSource("@multiplex").enableGlobalShortcuts()
+ updateMprisSourcesModel()
}
function togglePlaying() {
@@ -268,6 +277,31 @@
return service.startOperationCall(operation);
}
+ function updateMprisSourcesModel () {
+
+ var model = [{
+ 'text': i18n("Choose player automatically"),
+ 'icon': 'emblem-favorite',
+ '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
+ }
+
+ model.push({
+ 'text': mpris2Source.data[source]["Identity"],
+ 'icon': mpris2Source.data[source]["Desktop Icon Name"] || mpris2Source.data[source]["Desktop Entry"] || source,
+ 'source': source
+ });
+ }
+
+ root.mprisSourcesModel = model;
+ }
+
states: [
State {
name: "playing"