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,30 @@ +/*************************************************************************** + * Copyright 2020 Carson Black * + * * + * 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 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"] } } } 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,46 @@ +/*************************************************************************** + * Copyright 2020 Carson Black * + * * + * 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.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"