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 @@ -38,54 +38,42 @@ Layout.preferredHeight: Layout.minimumHeight * 1.5 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 - - property bool disablePositionUpdate: false property bool keyPressed: false - function retrievePosition() { - var service = mpris2Source.serviceForSource(mpris2Source.current); - var operation = service.operationDescription("GetPosition"); - service.startOperationCall(operation); - } + // only show hours (the default for KFormat) when track is actually longer than an hour + readonly property int durationFormattingOptions: Media.length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours Connections { target: plasmoid onExpandedChanged: { if (plasmoid.expanded) { - retrievePosition(); + Media.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 + Connections { + target: Media + function onSongLengthChanged(length) { + Media.lockPositionUpdate = true + { + seekSlider.value = 0 + seekSlider.to = Media.songLength + Media.retrievePosition() + } + Media.lockPositionUpdate = false + } + function onPositionChanged(position) { + // Don't interrupt an active drag. + if (!seekSlider.pressed && !keyPressed) { + Media.lockPositionUpdate = true + { + seekSlider.value = Media.position + } + Media.lockPositionUpdate = 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 @@ -98,31 +86,31 @@ if (event.key === Qt.Key_Space || event.key === Qt.Key_K) { // K is YouTube's key for "play/pause" :) - root.togglePlaying() + Media.togglePlaying() } else if (event.key === Qt.Key_P) { - root.action_previous() + Media.perform(Media.Actions.Previous) } else if (event.key === Qt.Key_N) { - root.action_next() + Media.perform(Media.Actions.Next) } else if (event.key === Qt.Key_S) { - root.action_stop() + Media.perform(Media.Actions.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 - seekSlider.moved(); + seekSlider.moved() } else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) { // seek forward 5s seekSlider.value = Math.min(seekSlider.to, seekSlider.value + 5000000) - seekSlider.moved(); + seekSlider.moved() } else if (event.key === Qt.Key_Home) { seekSlider.value = 0 - seekSlider.moved(); + seekSlider.moved() } else if (event.key === Qt.Key_End) { seekSlider.value = seekSlider.to - seekSlider.moved(); + 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(); + seekSlider.moved() } else { event.accepted = false } @@ -140,7 +128,7 @@ Image { id: backgroundImage - source: root.albumArt + source: Media.albumArt sourceSize.width: 512 /* * Setting a sourceSize.width here * prevents flickering when resizing the @@ -152,7 +140,7 @@ fillMode: Image.PreserveAspectCrop asynchronous: true - visible: !!root.track && status === Image.Ready && !softwareRendering + visible: Media.hasCurrentTrack && status === Image.Ready && !softwareRendering layer.enabled: !softwareRendering layer.effect: HueSaturation { @@ -194,24 +182,20 @@ anchors.fill: parent - visible: !!root.track && status === Image.Ready + visible: Media.hasAlbumArt && status === Image.Ready asynchronous: true horizontalAlignment: Image.AlignRight verticalAlignment: Image.AlignVCenter fillMode: Image.PreserveAspectFit - source: root.albumArt + source: Media.albumArt } PlasmaCore.IconItem { // Fallback visible: !albumArt.visible - source: { - if (mpris2Source.currentData["Desktop Icon Name"]) - return mpris2Source.currentData["Desktop Icon Name"] - return "media-album-cover" - } + source: Media.fallbackIcon anchors { fill: parent @@ -224,7 +208,7 @@ Layout.fillWidth: true Layout.fillHeight: true Layout.preferredWidth: 50 - Layout.alignment: !(albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"]) ? Qt.AlignHCenter : 0 + Layout.alignment: !(albumArt.visible || Media.current["Desktop Icon Name"]) ? Qt.AlignHCenter : 0 /* * We use Kirigami.Heading instead of PlasmaExtras.Heading @@ -242,14 +226,14 @@ fontSizeMode: Text.VerticalFit elide: Text.ElideRight - text: root.track || i18n("No media playing") + text: Media.hasCurrentTrack ? Media.currentTrack : i18n("No media playing") Layout.fillWidth: true Layout.maximumHeight: units.gridUnit*5 } Kirigami.Heading { // Song Artist id: songArtist - visible: root.track && root.artist + visible: Media.hasCurrentArtist level: 2 color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" @@ -259,7 +243,7 @@ fontSizeMode: Text.VerticalFit elide: Text.ElideRight - text: root.artist + text: Media.currentArtist Layout.fillWidth: true Layout.maximumHeight: units.gridUnit*2 } @@ -275,38 +259,7 @@ 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 "" - } + text: Media.currentAlbum Layout.fillWidth: true Layout.maximumHeight: units.gridUnit*2 } @@ -322,7 +275,7 @@ 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 && expandedRepresentation.length > 0 ? true : false + enabled: !Media.noPlayers && Media.hasCurrentTrack && Media.songLength > 0 opacity: enabled ? 1 : 0 Behavior on opacity { NumberAnimation { duration: units.longDuration } @@ -355,38 +308,38 @@ Layout.fillWidth: true z: 999 value: 0 - visible: canSeek + visible: Media.canSeek onMoved: { - if (!disablePositionUpdate) { + if (!Media.lockPositionUpdate) { // delay setting the position to avoid race conditions queuedPositionUpdate.restart() } } Timer { id: seekTimer - interval: 1000 / expandedRepresentation.rate + interval: 1000 / Media.playbackRate repeat: true - running: root.state === "playing" && plasmoid.expanded && !keyPressed && interval > 0 && seekSlider.to >= 1000000 + running: Media.state === "playing" && 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 + Media.lockPositionUpdate = true if (seekSlider.value == seekSlider.to) { - retrievePosition(); + Media.retrievePosition() } else { seekSlider.value += 1000000 } - disablePositionUpdate = false + Media.lockPositionUpdate = false } } } } RowLayout { - visible: !canSeek + visible: !Media.canSeek Layout.fillWidth: true Layout.preferredHeight: seekSlider.height @@ -417,7 +370,7 @@ Row { // Player Controls id: playerControls - property bool enabled: root.canControl + property bool enabled: Media.canControl property int controlsSize: theme.mSize(theme.defaultFont).height * 3 Layout.alignment: Qt.AlignHCenter @@ -427,31 +380,31 @@ anchors.verticalCenter: parent.verticalCenter width: expandedRepresentation.controlSize height: width - enabled: playerControls.enabled && root.canGoPrevious + enabled: playerControls.enabled && Media.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() + Media.perform(Media.Actions.Previous) } } 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() + enabled: Media.state == "playing" ? Media.canPause : Media.canPlay + icon.name: Media.state == "playing" ? "media-playback-pause" : "media-playback-start" + onClicked: Media.togglePlaying() } PlasmaComponents3.ToolButton { // Next anchors.verticalCenter: parent.verticalCenter width: expandedRepresentation.controlSize height: width - enabled: playerControls.enabled && root.canGoNext + enabled: playerControls.enabled && Media.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() + Media.perform(Media.Actions.Next) } } } @@ -464,38 +417,35 @@ id: playerCombo textRole: "text" visible: model.length > 2 // more than one player, @multiplex is always there - model: root.mprisSourcesModel + model: Media.sources 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) { + for (var i = 0; i < model.length; ++i) { + if (model[i].source === Media.source.current) { currentIndex = i break } } } onActivated: { - disablePositionUpdate = true + Media.lockPositionUpdate = true // ComboBox has currentIndex and currentText, why doesn't it have currentItem/currentModelValue? - mpris2Source.current = model[index].source - disablePositionUpdate = false + Media.currentSource = model[index].source + Media.lockPositionUpdate = false } } } Timer { id: queuedPositionUpdate interval: 100 onTriggered: { - if (position == seekSlider.value) { - return; + if (Media.position == seekSlider.value) { + return } - var service = mpris2Source.serviceForSource(mpris2Source.current) - var operation = service.operationDescription("SetPosition") - operation.microseconds = seekSlider.value - service.startOperationCall(operation) + Media.setPosition(seekSlider.value) } } } diff --git a/applets/mediacontroller/contents/ui/Media.qml b/applets/mediacontroller/contents/ui/Media.qml new file mode 100644 --- /dev/null +++ b/applets/mediacontroller/contents/ui/Media.qml @@ -0,0 +1,224 @@ +pragma Singleton + +import QtQuick 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore + +QtObject { + id: media + + property var sources: [] + + property alias source: mpris2Source + + property var current: mpris2Source.currentData || {} + + property alias currentSource: mpris2Source.currentSource + + readonly property var currentMetadata: mpris2Source.currentData ? mpris2Source.currentData.Metadata : {} + readonly property bool noPlayers: mpris2Source.sources.length <= 1 + readonly property var albumArt: currentMetadata ? currentMetadata["mpris:artUrl"] || "" : "" + readonly property var fallbackIcon: currentMetadata ? currentMetadata["Desktop Icon Name"] || "media-album-cover" : "media-album-cover" + + readonly property bool hasCurrentTrack: currentTrack != "" + readonly property bool hasAlbumArt: albumArt != "" && hasCurrentTrack + readonly property bool hasCurrentArtist: currentArtist != "" && hasCurrentTrack + readonly property string currentPlayer: !noPlayers ? current.Identity : "" + + property bool lockPositionUpdate: false + + readonly property string currentTrack: { + if (!currentMetadata) return "" + + var xesamTitle = currentMetadata["xesam:title"] + if (xesamTitle) return xesamTitle + + var xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" + if (!xesamUrl) return "" + + var lastSlashPos = xesamUrl.lastIndexOf('/') + if (lastSlashPos < 0) return "" + + var lastUrlPart = xesamUrl.substring(lastSlashPos + 1) + return decodeURIComponent(lastUrlPart) + } + readonly property string currentArtist: { + if (!currentMetadata) return "" + + var xesamArtist = currentMetadata["xesam:artist"] + if (!xesamArtist) return "" + + if (typeof xesamArtist == "string") { + return xesamArtist + } + return xesamArtist.join(", ") + } + readonly property string currentAlbum: { + if (!currentMetadata) return "" + + var xesamAlbum = currentMetadata["xesam:album"] + if (xesamAlbum) return xesamAlbum + + if (currentMetadata["xesam:title"] || currentArtist) return "" + + var xesamUrl = (currentMetadata["xesam:url"] || "").toString() + if (xesamUrl.indexOf("file:///") !== 0) 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 "" + } + + readonly property real playbackRate: current.Rate || 1 + readonly property double songLength: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 + readonly property bool canSeek: current.CanSeek || false + + readonly property bool canControl: (!noPlayers && current.CanControl) || false + readonly property bool canGoPrevious: (canControl && current.CanGoPrevious) || false + readonly property bool canGoNext: (canControl && current.CanGoNext) || false + readonly property bool canPlay: (canControl && current.CanPlay) || false + readonly property bool canPause: (canControl && current.CanPause) || false + + property double position: current.Position || 0 + + enum Actions { + Play, + Pause, + Next, + Previous, + Stop, + Raise, + Quit, + PlayPause + } + + // Helper function so we don't spam output logs for something that we + // expect can be null. + function silentNull(i) { + if (i == null) { + return {} + } + return i + } + + property QtObject dataSource: PlasmaCore.DataSource { + id: mpris2Source + + readonly property string multiplexSource: "@multiplex" + property string currentSource: multiplexSource + + readonly property var currentData: data[currentSource] + + engine: "mpris2" + connectedSources: sources + + onSourceAdded: media.updateSources() + onSourceRemoved: media.updateSources() + Component.onCompleted: media.updateSources() + } + + + function retrievePosition() { + var service = mpris2Source.serviceForSource(mpris2Source.currentSource) + var operation = service.operationDescription("GetPosition") + service.startOperationCall(operation) + } + + function setPosition(position) { + var service = mpris2Source.serviceForSource(mpris2Source.currentSource) + var operation = service.operationDescription("SetPosition") + operation.microseconds = position + service.startOperationCall(operation) + } + + function updateSources() { + media.sources = Array.from(mpris2Source.sources) + .filter(source => source !== mpris2Source.multiplexSource) + .map(source => { + return { + 'text': mpris2Source.data[source]["Identity"], + 'icon': mpris2Source.data[source]["Desktop Icon Name"] || mpris2Source.data[source]["Desktop Entry"] || source, + 'source': source + } + }) + .unshift({ + 'text': i18n("Choose player automatically"), + 'icon': 'emblem-favorite', + 'source': mpris2Source.multiplexSource + }) + } + + function serviceOp(src, op) { + var service = mpris2Source.serviceForSource(src) + var operation = service.operationDescription(op) + service.startOperationCall(operation) + } + + function adjustVolume(delta) { + let service = mpris2Source.serviceForSource(mpris2Source.current) + let operation = service.operationDescription("ChangeVolume") + operation.delta = delta + operation.showOSD = true + service.startOperationCall(operation) + } + + function perform(act) { + switch(act) { + case Media.Actions.Play: + serviceOp(mpris2Source.currentSource, "Play") + break + case Media.Actions.Pause: + serviceOp(mpris2Source.currentSource, "Pause") + break + case Media.Actions.Previous: + serviceOp(mpris2Source.currentSource, "Previous") + break + case Media.Actions.Next: + serviceOp(mpris2Source.currentSource, "Next") + break + case Media.Actions.Stop: + serviceOp(mpris2Source.current, "Stop") + break + case Media.Actions.Raise: + serviceOp(mpris2Source.current, "Raise") + break + case Media.Actions.Quit: + serviceOp(mpris2Source.current, "Quit") + break + case Media.Actions.PlayPause: + serviceOp(mpris2Source.current, "PlayPause") + break + default: + print("Unhandled action: " + act) + } + } + + function togglePlaying() { + if (Media.state === "playing" && Media.canPause) { + Media.perform(Media.Actions.Pause) + } else if (Media.canPlay) { + Media.perform(Media.Actions.Play) + } + } + + function action_open() { Media.perform(Media.Actions.Raise) } + function action_quit() { Media.perform(Media.Actions.Quit) } + function action_play() { Media.perform(Media.Actions.Play) } + function action_pause() { Media.perform(Media.Actions.Pause) } + function action_playPause() { Media.perform(Media.Actions.PlayPause) } + function action_previous() { Media.perform(Media.Actions.Previous) } + function action_next() { Media.perform(Media.Actions.Next) } + function action_stop() { Media.perform(Media.Actions.Stop) } + + property string state: { + if (!media.noPlayer && silentNull(media.current).PlaybackStatus === "Playing") { + return "playing" + } else if (!media.noPlayer && silentNull(media.current).PlaybackStatus === "Paused") { + return "paused" + } + return "" + } +} 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 @@ -29,68 +29,18 @@ Item { id: root - property var currentMetadata: mpris2Source.currentData ? mpris2Source.currentData.Metadata : undefined - property string track: { - if (!currentMetadata) { - return "" - } - var xesamTitle = currentMetadata["xesam:title"] - if (xesamTitle) { - return xesamTitle - } - // if no track title is given, print out the file name - var xesamUrl = currentMetadata["xesam:url"] ? currentMetadata["xesam:url"].toString() : "" - if (!xesamUrl) { - return "" - } - var lastSlashPos = xesamUrl.lastIndexOf('/') - if (lastSlashPos < 0) { - return "" - } - var lastUrlPart = xesamUrl.substring(lastSlashPos + 1) - return decodeURIComponent(lastUrlPart) - } - property string artist: { - if (!currentMetadata) { - return "" - } - var xesamArtist = currentMetadata["xesam:artist"] - if (!xesamArtist) { - return ""; - } - - if (typeof xesamArtist == "string") { - return xesamArtist - } else { - return xesamArtist.join(", ") - } - } - property string albumArt: currentMetadata ? currentMetadata["mpris:artUrl"] || "" : "" - - readonly property string identity: !root.noPlayer ? mpris2Source.currentData.Identity || mpris2Source.current : "" - - 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 - readonly property bool canPlay: (canControl && mpris2Source.currentData.CanPlay) || false - readonly property bool canPause: (canControl && mpris2Source.currentData.CanPause) || false - Plasmoid.switchWidth: units.gridUnit * 14 Plasmoid.switchHeight: units.gridUnit * 10 Plasmoid.icon: "media-playback-playing" Plasmoid.toolTipMainText: i18n("No media playing") - Plasmoid.toolTipSubText: identity + Plasmoid.toolTipSubText: Media.currentPlayer Plasmoid.toolTipTextFormat: Text.PlainText Plasmoid.status: PlasmaCore.Types.PassiveStatus Plasmoid.onContextualActionsAboutToShow: { plasmoid.clearActions() - if (root.noPlayer) { + if (Media.noPlayers) { return } @@ -101,46 +51,46 @@ if (canControl) { plasmoid.setAction("previous", i18nc("Play previous track", "Previous Track"), - Qt.application.layoutDirection === Qt.RightToLeft ? "media-skip-forward" : "media-skip-backward"); + Qt.application.layoutDirection === Qt.RightToLeft ? "media-skip-forward" : "media-skip-backward") plasmoid.action("previous").enabled = Qt.binding(function() { - return root.canGoPrevious + return Media.canGoPrevious }) // if CanPause, toggle the menu entry between Play & Pause, otherwise always use Play - if (root.state == "playing" && root.canPause) { + if (Media.state == "playing" && Media.canPause) { plasmoid.setAction("pause", i18nc("Pause playback", "Pause"), "media-playback-pause") plasmoid.action("pause").enabled = Qt.binding(function() { - return root.state === "playing" && root.canPause; - }); + return Media.state === "playing" && Media.canPause + }) } else { plasmoid.setAction("play", i18nc("Start playback", "Play"), "media-playback-start") plasmoid.action("play").enabled = Qt.binding(function() { - return root.state !== "playing" && root.canPlay; - }); + return Media.state !== "playing" && Media.canPlay + }) } plasmoid.setAction("next", i18nc("Play next track", "Next Track"), Qt.application.layoutDirection === Qt.RightToLeft ? "media-skip-backward" : "media-skip-forward") plasmoid.action("next").enabled = Qt.binding(function() { - return root.canGoNext + return Media.canGoNext }) plasmoid.setAction("stop", i18nc("Stop playback", "Stop"), "media-playback-stop") plasmoid.action("stop").enabled = Qt.binding(function() { - return root.state === "playing" || root.state === "paused"; + return Media.state === "playing" || Media.state === "paused" }) } if (mpris2Source.currentData.CanQuit) { - plasmoid.setActionSeparator("quitseparator"); + plasmoid.setActionSeparator("quitseparator") plasmoid.setAction("quit", i18nc("Quit player", "Quit"), "application-exit") } } // HACK Some players like Amarok take quite a while to load the next track // this avoids having the plasmoid jump between popup and panel onStateChanged: { - if (state != "") { + if (Media.state != "") { plasmoid.status = PlasmaCore.Types.ActiveStatus } else { updatePlasmoidStatusTimer.restart() @@ -151,7 +101,7 @@ id: updatePlasmoidStatusTimer interval: 3000 onTriggered: { - if (state != "") { + if (Media.state != "") { plasmoid.status = PlasmaCore.Types.ActiveStatus } else { plasmoid.status = PlasmaCore.Types.PassiveStatus @@ -162,167 +112,61 @@ Plasmoid.fullRepresentation: ExpandedRepresentation {} Plasmoid.compactRepresentation: PlasmaCore.IconItem { - source: root.state === "playing" ? "media-playback-playing" : - root.state === "paused" ? "media-playback-paused" : - "media-playback-stopped" + source: { + if (Media.state === "playing") { + return "media-playback-playing" + } else if (Media.state == "paused") { + return "media-playback-paused" + } else { + return "media-playback-stopped" + } + } active: compactMouse.containsMouse MouseArea { id: compactMouse anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton - onWheel: { - var service = mpris2Source.serviceForSource(mpris2Source.current) - var operation = service.operationDescription("ChangeVolume") - operation.delta = (wheel.angleDelta.y / 120) * 0.03 - operation.showOSD = true - service.startOperationCall(operation) - } + onWheel: Media.adjustVolume((wheel.angleDelta.y / 120) * 0.03) onClicked: { switch (mouse.button) { case Qt.MiddleButton: - root.togglePlaying() + Media.togglePlaying() break case Qt.BackButton: - root.action_previous() + Media.perform(Media.previous) break case Qt.ForwardButton: - root.action_next() + Media.perform(Media.next) break default: plasmoid.expanded = !plasmoid.expanded } } } } - PlasmaCore.DataSource { - id: mpris2Source - - readonly property string multiplexSource: "@multiplex" - property string current: multiplexSource - - readonly property var currentData: data[current] - - engine: "mpris2" - 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() - updateMprisSourcesModel() - } - - function togglePlaying() { - if (root.state === "playing") { - if (root.canPause) { - root.action_pause(); - } - } else { - if (root.canPlay) { - root.action_play(); - } - } - } - - function action_open() { - serviceOp(mpris2Source.current, "Raise"); - } - function action_quit() { - serviceOp(mpris2Source.current, "Quit"); - } - - function action_play() { - serviceOp(mpris2Source.current, "Play"); - } - - function action_pause() { - serviceOp(mpris2Source.current, "Pause"); - } - - function action_playPause() { - serviceOp(mpris2Source.current, "PlayPause"); - } - - function action_previous() { - serviceOp(mpris2Source.current, "Previous"); - } - - function action_next() { - serviceOp(mpris2Source.current, "Next"); - } - - function action_stop() { - serviceOp(mpris2Source.current, "Stop"); - } - - function serviceOp(src, op) { - var service = mpris2Source.serviceForSource(src); - var operation = service.operationDescription(op); - 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; - } - + state: Media.state states: [ State { name: "playing" - when: !root.noPlayer && mpris2Source.currentData.PlaybackStatus === "Playing" PropertyChanges { target: plasmoid - icon: albumArt ? albumArt : "media-playback-playing" - toolTipMainText: track - toolTipSubText: artist ? i18nc("by Artist (player name)", "by %1 (%2)", artist, identity) : identity + toolTipMainText: Media.currentTrack + toolTipSubText: Media.currentArtist ? i18nc("by Artist (player name)", "by %1 (%2)", Media.currentArtist, Media.currentPlayer) : Media.currentPlayer } }, State { name: "paused" - when: !root.noPlayer && mpris2Source.currentData.PlaybackStatus === "Paused" PropertyChanges { target: plasmoid - icon: albumArt ? albumArt : "media-playback-paused" - toolTipMainText: track - toolTipSubText: artist ? i18nc("by Artist (paused, player name)", "by %1 (paused, %2)", artist, identity) : i18nc("Paused (player name)", "Paused (%1)", identity) + toolTipMainText: Media.currentTrack + toolTipSubText: Media.currentArtist ? i18nc("by Artist (paused, player name)", "by %1 (paused, %2)", Media.currentArtist, Media.currentPlayer) : i18nc("Paused (player name)", "Paused (%1)", Media.currentPlayer) } } ] diff --git a/applets/mediacontroller/contents/ui/qmldir b/applets/mediacontroller/contents/ui/qmldir new file mode 100644 --- /dev/null +++ b/applets/mediacontroller/contents/ui/qmldir @@ -0,0 +1 @@ +singleton Media 1.0 Media.qml