Changeset View
Standalone View
applets/mediacontroller/contents/ui/ExpandedRepresentation.qml
1 | /*************************************************************************** | 1 | /*************************************************************************** | ||
---|---|---|---|---|---|
2 | * Copyright 2013 Sebastian Kügler <sebas@kde.org> * | 2 | * Copyright 2013 Sebastian Kügler <sebas@kde.org> * | ||
3 | * Copyright 2014, 2016 Kai Uwe Broulik <kde@privat.broulik.de> * | 3 | * Copyright 2014, 2016 Kai Uwe Broulik <kde@privat.broulik.de> * | ||
4 | * Copyright 2020 Carson Black <uhhadd@gmail.com> * | ||||
5 | * Copyright 2020 Ismael Asensio <isma.af@gmail.com> * | ||||
4 | * * | 6 | * * | ||
5 | * This program is free software; you can redistribute it and/or modify * | 7 | * This program is free software; you can redistribute it and/or modify * | ||
6 | * it under the terms of the GNU Library General Public License as * | 8 | * it under the terms of the GNU Library General Public License as * | ||
7 | * published by the Free Software Foundation; either version 2 of the * | 9 | * published by the Free Software Foundation; either version 2 of the * | ||
8 | * License, or (at your option) any later version. * | 10 | * License, or (at your option) any later version. * | ||
9 | * * | 11 | * * | ||
10 | * This program is distributed in the hope that it will be useful, * | 12 | * This program is distributed in the hope that it will be useful, * | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * | 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | ||
13 | * GNU Library General Public License for more details. * | 15 | * GNU Library General Public License for more details. * | ||
14 | * * | 16 | * * | ||
15 | * You should have received a copy of the GNU Library General Public * | 17 | * You should have received a copy of the GNU Library General Public * | ||
16 | * License along with this program; if not, write to the * | 18 | * License along with this program; if not, write to the * | ||
17 | * Free Software Foundation, Inc., * | 19 | * Free Software Foundation, Inc., * | ||
18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * | 20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * | ||
19 | ***************************************************************************/ | 21 | ***************************************************************************/ | ||
20 | 22 | | |||
21 | import QtQuick 2.4 | 23 | import QtQuick 2.8 | ||
22 | import QtQuick.Layouts 1.1 | 24 | import QtQuick.Layouts 1.1 | ||
23 | import org.kde.plasma.core 2.0 as PlasmaCore | 25 | import org.kde.plasma.core 2.0 as PlasmaCore | ||
24 | import org.kde.plasma.components 3.0 as PlasmaComponents3 | 26 | import org.kde.plasma.components 3.0 as PlasmaComponents3 | ||
25 | import org.kde.plasma.extras 2.0 as PlasmaExtras | 27 | import org.kde.plasma.extras 2.0 as PlasmaExtras | ||
26 | import org.kde.kcoreaddons 1.0 as KCoreAddons | 28 | import org.kde.kcoreaddons 1.0 as KCoreAddons | ||
29 | import QtGraphicalEffects 1.0 | ||||
27 | 30 | | |||
28 | Item { | 31 | Item { | ||
29 | id: expandedRepresentation | 32 | id: expandedRepresentation | ||
30 | 33 | | |||
31 | Layout.minimumWidth: Layout.minimumHeight * 1.333 | 34 | Layout.minimumWidth: units.gridUnit * 15 | ||
32 | Layout.minimumHeight: units.gridUnit * 10 | 35 | Layout.minimumHeight: units.gridUnit * 23 | ||
33 | Layout.preferredWidth: Layout.minimumWidth * 1.5 | 36 | Layout.preferredWidth: Layout.minimumWidth * 1.5 | ||
34 | Layout.preferredHeight: Layout.minimumHeight * 1.5 | 37 | Layout.preferredHeight: Layout.minimumHeight * 1.5 | ||
35 | 38 | | |||
39 | Layout.margins: units.largeSpacing | ||||
40 | | ||||
36 | readonly property int controlSize: units.iconSizes.large | 41 | readonly property int controlSize: units.iconSizes.large | ||
37 | 42 | | |||
38 | property double position: mpris2Source.currentData.Position || 0 | 43 | property double position: mpris2Source.currentData.Position || 0 | ||
39 | readonly property real rate: mpris2Source.currentData.Rate || 1 | 44 | readonly property real rate: mpris2Source.currentData.Rate || 1 | ||
40 | readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 | 45 | readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 | ||
41 | readonly property bool canSeek: mpris2Source.currentData.CanSeek || false | 46 | readonly property bool canSeek: mpris2Source.currentData.CanSeek || false | ||
47 | readonly property bool softwareRendering: GraphicsInfo.api === GraphicsInfo.Software | ||||
42 | 48 | | |||
43 | // only show hours (the default for KFormat) when track is actually longer than an hour | 49 | // only show hours (the default for KFormat) when track is actually longer than an hour | ||
44 | readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours | 50 | readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours | ||
45 | 51 | | |||
46 | property bool disablePositionUpdate: false | 52 | property bool disablePositionUpdate: false | ||
47 | property bool keyPressed: false | 53 | property bool keyPressed: false | ||
48 | 54 | | |||
49 | function retrievePosition() { | 55 | function retrievePosition() { | ||
▲ Show 20 Lines • Show All 69 Lines • ▼ Show 20 Line(s) | 123 | } else if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) { | |||
119 | seekSlider.value = seekSlider.to * (event.key - Qt.Key_0) / 10 | 125 | seekSlider.value = seekSlider.to * (event.key - Qt.Key_0) / 10 | ||
120 | seekSlider.moved(); | 126 | seekSlider.moved(); | ||
121 | } else { | 127 | } else { | ||
122 | event.accepted = false | 128 | event.accepted = false | ||
123 | } | 129 | } | ||
124 | } | 130 | } | ||
125 | } | 131 | } | ||
126 | 132 | | |||
127 | PlasmaComponents3.ComboBox { | 133 | ColumnLayout { // Main Column Layout | ||
128 | id: playerCombo | 134 | id: mainCol | ||
129 | width: Math.round(0.6 * parent.width) | 135 | anchors.fill: parent | ||
130 | height: visible ? undefined : 0 | | |||
131 | anchors.horizontalCenter: parent.horizontalCenter | | |||
132 | textRole: "text" | | |||
133 | visible: model.length > 2 // more than one player, @multiplex is always there | | |||
134 | model: { | | |||
135 | var model = [{ | | |||
136 | text: i18n("Choose player automatically"), | | |||
137 | source: mpris2Source.multiplexSource | | |||
138 | }] | | |||
139 | 136 | | |||
140 | var sources = mpris2Source.sources | 137 | Item { // Album Art Background + Details | ||
141 | for (var i = 0, length = sources.length; i < length; ++i) { | 138 | Layout.margins: units.smallSpacing | ||
142 | var source = sources[i] | 139 | Layout.fillWidth: true | ||
broulik: Superfluous since you fill both width and height? | |||||
143 | if (source === mpris2Source.multiplexSource) { | 140 | Layout.fillHeight: true | ||
144 | continue | | |||
145 | } | | |||
146 | 141 | | |||
147 | // we could show the pretty player name ("Identity") here but then we | 142 | Image { | ||
148 | // would have to connect all sources just for this | 143 | id: backgroundImage | ||
149 | model.push({text: source, source: source}) | | |||
150 | } | | |||
151 | 144 | | |||
152 | return model | 145 | source: root.albumArt | ||
153 | } | 146 | sourceSize.width: 512 /* Setting a source size means the item doesn't need | ||
147 | to recompute blur as the user resizes the plasmoid | ||||
148 | Additionally, it puts a bit of a cap on how large the | ||||
149 | buffer getting blurred can be, saving resources. | ||||
150 | */ | ||||
Do you have a source for this claim? From what I know blur merely blurs the actual item texture which is rendered, no the raw image source? broulik: Do you have a source for this claim? From what I know blur merely blurs the actual item texture… | |||||
cblack: https://github. | |||||
That says the opposite of what you want. We want image's texture provider to be used directly. Only then will this sourcesize matter. This is saying when layer effect is enabled it uses the base blitting path. We don't want to use layer here, we want the image as a source to the blur directly davidedmundson: That says the opposite of what you want.
We want image's texture provider to be used directly. | |||||
151 | | ||||
152 | anchors.fill: parent | ||||
153 | anchors.margins: -units.smallSpacing*2 | ||||
154 | fillMode: Image.PreserveAspectCrop | ||||
155 | | ||||
156 | asynchronous: true | ||||
157 | visible: !!root.track && status === Image.Ready && !softwareRendering | ||||
158 | | ||||
159 | layer.enabled: !softwareRendering | ||||
160 | layer.effect: HueSaturation { | ||||
161 | cached: true | ||||
162 | | ||||
163 | lightness: -0.5 | ||||
164 | saturation: 0.9 | ||||
154 | 165 | | |||
155 | onModelChanged: { | 166 | layer.enabled: true | ||
156 | // if model changes, ComboBox resets, so we try to find the current player again... | 167 | layer.effect: GaussianBlur { | ||
157 | for (var i = 0, length = model.length; i < length; ++i) { | 168 | cached: true | ||
158 | if (model[i].source === mpris2Source.current) { | 169 | | ||
159 | currentIndex = i | 170 | radius: 256 | ||
160 | break | 171 | deviation: 12 | ||
172 | samples: 129 | ||||
173 | | ||||
174 | transparentBorder: false | ||||
161 | } | 175 | } | ||
162 | } | 176 | } | ||
163 | } | 177 | } | ||
178 | RowLayout { // Album Art + Details | ||||
179 | id: albumRow | ||||
180 | spacing: units.largeSpacing | ||||
164 | 181 | | |||
165 | onActivated: { | 182 | anchors.fill: parent | ||
166 | disablePositionUpdate = true | | |||
167 | // ComboBox has currentIndex and currentText, why doesn't it have currentItem/currentModelValue? | | |||
168 | mpris2Source.current = model[index].source | | |||
169 | disablePositionUpdate = false | | |||
170 | } | | |||
171 | } | | |||
172 | 183 | | |||
173 | Item { | 184 | Item { | ||
185 | Layout.fillWidth: true | ||||
186 | Layout.fillHeight: true | ||||
187 | Layout.preferredWidth: parent.width / 2 | ||||
188 | Layout.maximumWidth: parent.width / 2 | ||||
189 | | ||||
190 | visible: albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"] | ||||
191 | | ||||
192 | Image { // Album Art | ||||
193 | id: albumArt | ||||
194 | | ||||
174 | anchors { | 195 | anchors { | ||
ngraham: whitespace | |||||
cblack: Whitespace is intentional to help with code legibility | |||||
175 | horizontalCenter: parent.horizontalCenter | 196 | fill: parent | ||
176 | top: playerCombo.bottom | | |||
177 | bottom: controlCol.top | | |||
178 | margins: units.smallSpacing | 197 | margins: units.smallSpacing | ||
ngraham: whitespace | |||||
179 | } | 198 | } | ||
180 | 199 | | |||
181 | PlasmaCore.IconItem { | 200 | visible: !!root.track && status === Image.Ready | ||
201 | | ||||
202 | asynchronous: true | ||||
203 | | ||||
204 | horizontalAlignment: Image.AlignRight | ||||
205 | verticalAlignment: Image.AlignVCenter | ||||
206 | fillMode: Image.PreserveAspectFit | ||||
207 | | ||||
208 | source: root.albumArt | ||||
209 | } | ||||
210 | | ||||
211 | PlasmaCore.IconItem { // Fallback | ||||
212 | visible: !albumArt.visible && !!mpris2Source.currentData["Desktop Icon Name"] | ||||
213 | source: mpris2Source.currentData["Desktop Icon Name"] | ||||
214 | | ||||
182 | anchors { | 215 | anchors { | ||
183 | horizontalCenter: parent.horizontalCenter | 216 | fill: parent | ||
184 | verticalCenter: parent.verticalCenter | 217 | margins: units.smallSpacing | ||
185 | } | 218 | } | ||
186 | 219 | | |||
187 | height: Math.round(parent.height / 2) | | |||
188 | width: height | | |||
189 | 220 | | |||
190 | source: mpris2Source.currentData["Desktop Icon Name"] | 221 | } | ||
191 | visible: !albumArt.visible | 222 | } | ||
223 | | ||||
224 | ColumnLayout { // Details Column | ||||
225 | Layout.fillWidth: true | ||||
226 | Layout.fillHeight: true | ||||
227 | Layout.preferredWidth: parent.width / 2 | ||||
228 | Layout.maximumWidth: parent.width / 2 | ||||
229 | Layout.alignment: !(albumArt.visible || !!mpris2Source.currentData["Desktop Icon Name"]) ? Qt.AlignHCenter : 0 | ||||
230 | | ||||
231 | PlasmaExtras.Heading { // Song Title | ||||
232 | id: songTitle | ||||
233 | level: 1 | ||||
234 | | ||||
235 | color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" | ||||
Does assigning undefined work? I don't see a RESET property for it. Probably want to assign color scope text color instead? broulik: Does assigning `undefined` work? I don't see a `RESET` property for it. Probably want to assign… | |||||
192 | 236 | | |||
193 | usesPlasmaTheme: false | 237 | textFormat: Text.PlainText | ||
238 | wrapMode: Text.Wrap | ||||
239 | fontSizeMode: Text.VerticalFit | ||||
240 | | ||||
241 | text: root.track || i18n("No media playing") | ||||
242 | | ||||
243 | Layout.maximumWidth: parent.width | ||||
244 | Layout.maximumHeight: units.gridUnit*5 | ||||
194 | } | 245 | } | ||
246 | PlasmaExtras.Heading { // Song Artist | ||||
247 | id: songArtist | ||||
Is a hardcoded white color always guaranteed to look good even when the album art is very light? Is the blurred background always going to be dark enough? ngraham: Is a hardcoded white color always guaranteed to look good even when the album art is very light? | |||||
cblack: {F8085988} | |||||
ngraham: All right, looks good! | |||||
gvgeo: With a white theme? | |||||
248 | visible: root.track && root.artist | ||||
249 | level: 2 | ||||
gvgeo: You are limiting `Details Column`'s width, while `Album Art` is not always visible. | |||||
broulik: Make sure to `Math.round` random factors of a size | |||||
250 | | ||||
251 | color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" | ||||
ngraham: I think that's the default, no? | |||||
252 | | ||||
253 | textFormat: Text.PlainText | ||||
254 | wrapMode: Text.Wrap | ||||
255 | fontSizeMode: Text.VerticalFit | ||||
256 | | ||||
257 | text: root.artist | ||||
258 | Layout.maximumWidth: parent.width | ||||
259 | Layout.maximumHeight: units.gridUnit*2 | ||||
195 | } | 260 | } | ||
261 | PlasmaExtras.Heading { // Song Album | ||||
there's a massive binding loop somewhere here: file:///home/nate/kde/usr/share/plasma/plasmoids/org.kde.plasma.mediacontroller/contents/ui/ExpandedRepresentation.qml:261:21: QML Heading: Binding loop detected for property "height" ngraham: there's a massive binding loop somewhere here:
```
file… | |||||
cblack: I can't seem to reproduce a binding loop here. | |||||
I have not seen your code yet, I will check tomorrow. Probably because you have something like this: Layout.preferredWidth: parent.width / 2 Layout.preferredHeight: parent.height / 2 Check the last answer from: How to design a multi-level fluid layout in QML. In short words: when possible, do not use Layout.preferredWidth, prefer implicitWidth instead. If you want to have 50-50 ratio between items, set implicitWidth in both items to the same value. kmaterka: I have not seen your code yet, I will check tomorrow. Probably because you have something like… | |||||
My last comment was not precise. Just check this for all details :) kmaterka: My last comment was not precise. Just check [[ https://stackoverflow.com/a/52754479 | this ]]… | |||||
As a general rule, you shouldn't refer to parents width in Layouts, including: Layout.maximumWidth: parent.width. It a (very) common mistake :) kmaterka: As a general rule, you shouldn't refer to parents width in Layouts, including: `Layout. | |||||
This advice doesn't seem to be applicable here, as the implicitWidth of the ColumnLayout child isn't able to be mutated due to the fact that all of its child items (text) have fixed implicitWidths. cblack: This advice doesn't seem to be applicable here, as the `implicitWidth` of the ColumnLayout… | |||||
True, I didn't want to copy whole answer from Stack Overflow :) In this case Layout.preferredWidth should not be set to parent.width / 2, better to use Layout.preferredWidth = 50. I sometimes add % in a comment: Layout.preferredWidth = 50//%. kmaterka: True, I didn't want to copy whole answer from Stack Overflow :) In this case `Layout. | |||||
262 | color: (softwareRendering || !albumArt.visible) ? PlasmaCore.ColorScope.textColor : "white" | ||||
196 | 263 | | |||
197 | Image { | 264 | level: 3 | ||
198 | id: albumArt | 265 | opacity: 0.6 | ||
199 | anchors { | 266 | | ||
200 | left: parent.left | 267 | textFormat: Text.PlainText | ||
201 | right: parent.right | 268 | wrapMode: Text.Wrap | ||
202 | top: playerCombo.bottom | 269 | fontSizeMode: Text.VerticalFit | ||
203 | bottom: controlCol.top | 270 | | ||
204 | margins: units.smallSpacing | 271 | visible: text.length !== 0 | ||
272 | text: { | ||||
273 | var metadata = root.currentMetadata | ||||
274 | if (!metadata) { | ||||
275 | return "" | ||||
205 | } | 276 | } | ||
206 | source: root.albumArt | 277 | var xesamAlbum = metadata["xesam:album"] | ||
207 | asynchronous: true | 278 | if (xesamAlbum) { | ||
208 | fillMode: Image.PreserveAspectFit | 279 | return xesamAlbum | ||
209 | sourceSize: Qt.size(height, height) | | |||
210 | visible: !!root.track && status === Image.Ready | | |||
211 | } | 280 | } | ||
212 | 281 | | |||
213 | Column { | 282 | // if we play a local file without title and artist, show its containing folder instead | ||
214 | id: controlCol | 283 | if (metadata["xesam:title"] || root.artist) { | ||
215 | width: parent.width | 284 | return "" | ||
216 | anchors.bottom: parent.bottom | 285 | } | ||
217 | 286 | | |||
218 | spacing: units.smallSpacing | 287 | var xesamUrl = (metadata["xesam:url"] || "").toString() | ||
288 | if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" | ||||
289 | return "" | ||||
290 | } | ||||
219 | 291 | | |||
220 | RowLayout { | 292 | var urlParts = xesamUrl.split("/") | ||
221 | anchors { | 293 | if (urlParts.length < 3) { | ||
222 | left: parent.left | 294 | return "" | ||
223 | right: parent.right | | |||
224 | margins: units.smallSpacing | | |||
225 | } | 295 | } | ||
226 | 296 | | |||
297 | var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename | ||||
298 | if (lastFolderPath) { | ||||
ngraham: `text.length !== 0` is a bit faster | |||||
299 | return lastFolderPath | ||||
300 | } | ||||
301 | | ||||
302 | return "" | ||||
303 | } | ||||
304 | Layout.maximumWidth: parent.width | ||||
305 | Layout.maximumHeight: units.gridUnit*2 | ||||
306 | } | ||||
307 | } | ||||
308 | } | ||||
309 | } | ||||
310 | | ||||
311 | RowLayout { // Seek Bar | ||||
227 | spacing: units.smallSpacing | 312 | spacing: units.smallSpacing | ||
228 | 313 | | |||
229 | // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case | 314 | // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case | ||
230 | enabled: !root.noPlayer && root.track && expandedRepresentation.length > 0 ? true : false | 315 | enabled: !root.noPlayer && root.track && expandedRepresentation.length > 0 ? true : false | ||
231 | opacity: enabled ? 1 : 0 | 316 | opacity: enabled ? 1 : 0 | ||
232 | Behavior on opacity { | 317 | Behavior on opacity { | ||
233 | NumberAnimation { duration: units.longDuration } | 318 | NumberAnimation { duration: units.longDuration } | ||
234 | } | 319 | } | ||
235 | 320 | | |||
321 | Layout.alignment: Qt.AlignHCenter | ||||
322 | Layout.fillWidth: true | ||||
323 | Layout.maximumWidth: Math.min(units.gridUnit*45, Math.round(expandedRepresentation.width*(7/10))) | ||||
If the user does something a bit silly like making the applet as big as the whole screen, they might prefer if the seek bar is really long ngraham: If the user does something a bit silly like making the applet as big as the whole screen, they… | |||||
broulik: No magic numbers, please | |||||
324 | | ||||
236 | // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song | 325 | // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song | ||
237 | TextMetrics { | 326 | TextMetrics { | ||
238 | id: timeMetrics | 327 | id: timeMetrics | ||
239 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | 328 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | ||
240 | KCoreAddons.Format.formatDuration(seekSlider.to / 1000, expandedRepresentation.durationFormattingOptions)) | 329 | KCoreAddons.Format.formatDuration(seekSlider.to / 1000, expandedRepresentation.durationFormattingOptions)) | ||
241 | font: theme.smallestFont | 330 | font: theme.smallestFont | ||
242 | } | 331 | } | ||
243 | 332 | | |||
244 | PlasmaComponents3.Label { | 333 | PlasmaComponents3.Label { // Time Elapsed | ||
245 | Layout.preferredWidth: timeMetrics.width | 334 | Layout.preferredWidth: timeMetrics.width | ||
246 | verticalAlignment: Text.AlignVCenter | 335 | verticalAlignment: Text.AlignVCenter | ||
247 | horizontalAlignment: Text.AlignRight | 336 | horizontalAlignment: Text.AlignRight | ||
248 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) | 337 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) | ||
249 | opacity: 0.9 | 338 | opacity: 0.9 | ||
250 | font: theme.smallestFont | 339 | font: theme.smallestFont | ||
340 | color: PlasmaCore.ColorScope.textColor | ||||
broulik: The labels are shown outside the album art, so they are unreadable now | |||||
251 | } | 341 | } | ||
252 | 342 | | |||
253 | PlasmaComponents3.Slider { | 343 | PlasmaComponents3.Slider { // Slider | ||
254 | id: seekSlider | 344 | id: seekSlider | ||
255 | Layout.fillWidth: true | 345 | Layout.fillWidth: true | ||
256 | z: 999 | 346 | z: 999 | ||
257 | value: 0 | 347 | value: 0 | ||
258 | visible: canSeek | 348 | visible: canSeek | ||
259 | 349 | | |||
260 | onMoved: { | 350 | onMoved: { | ||
261 | if (!disablePositionUpdate) { | 351 | if (!disablePositionUpdate) { | ||
Show All 18 Lines | 369 | } else { | |||
280 | seekSlider.value += 1000000 | 370 | seekSlider.value += 1000000 | ||
281 | } | 371 | } | ||
282 | disablePositionUpdate = false | 372 | disablePositionUpdate = false | ||
283 | } | 373 | } | ||
284 | } | 374 | } | ||
285 | } | 375 | } | ||
286 | } | 376 | } | ||
287 | 377 | | |||
288 | PlasmaComponents3.ProgressBar { | 378 | PlasmaComponents3.ProgressBar { // Time Remaining | ||
289 | Layout.fillWidth: true | 379 | Layout.fillWidth: true | ||
380 | Layout.preferredHeight: seekSlider.height | ||||
290 | value: seekSlider.value | 381 | value: seekSlider.value | ||
291 | from: seekSlider.from | 382 | from: seekSlider.from | ||
292 | to: seekSlider.to | 383 | to: seekSlider.to | ||
293 | visible: !canSeek | 384 | visible: !canSeek | ||
294 | } | 385 | } | ||
295 | 386 | | |||
296 | PlasmaComponents3.Label { | 387 | PlasmaComponents3.Label { | ||
297 | Layout.preferredWidth: timeMetrics.width | 388 | Layout.preferredWidth: timeMetrics.width | ||
298 | verticalAlignment: Text.AlignVCenter | 389 | verticalAlignment: Text.AlignVCenter | ||
299 | horizontalAlignment: Text.AlignLeft | 390 | horizontalAlignment: Text.AlignLeft | ||
300 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | 391 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | ||
301 | KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) | 392 | KCoreAddons.Format.formatDuration((seekSlider.to - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) | ||
302 | opacity: 0.9 | 393 | opacity: 0.9 | ||
303 | font: theme.smallestFont | 394 | font: theme.smallestFont | ||
395 | color: PlasmaCore.ColorScope.textColor | ||||
304 | } | 396 | } | ||
305 | } | 397 | } | ||
306 | 398 | | |||
307 | Column { | 399 | Row { // Player Controls | ||
308 | width: parent.width | | |||
309 | | ||||
310 | PlasmaExtras.Heading { | | |||
311 | id: song | | |||
312 | width: parent.width | | |||
313 | height: undefined | | |||
314 | level: 4 | | |||
315 | horizontalAlignment: Text.AlignHCenter | | |||
316 | | ||||
317 | maximumLineCount: 1 | | |||
318 | elide: Text.ElideRight | | |||
319 | text: { | | |||
320 | if (!root.track) { | | |||
321 | return i18n("No media playing") | | |||
322 | } | | |||
323 | return root.artist ? i18nc("artist – track", "%1 – %2", root.artist, root.track) : root.track | | |||
324 | } | | |||
325 | textFormat: Text.PlainText | | |||
326 | } | | |||
327 | | ||||
328 | PlasmaExtras.Heading { | | |||
329 | width: parent.width | | |||
330 | height: undefined | | |||
331 | level: 5 | | |||
332 | opacity: 0.6 | | |||
333 | horizontalAlignment: Text.AlignHCenter | | |||
334 | wrapMode: Text.NoWrap | | |||
335 | elide: Text.ElideRight | | |||
336 | visible: text !== "" | | |||
337 | text: { | | |||
338 | var metadata = root.currentMetadata | | |||
339 | if (!metadata) { | | |||
340 | return "" | | |||
341 | } | | |||
342 | var xesamAlbum = metadata["xesam:album"] | | |||
343 | if (xesamAlbum) { | | |||
344 | return xesamAlbum | | |||
345 | } | | |||
346 | | ||||
347 | // if we play a local file without title and artist, show its containing folder instead | | |||
348 | if (metadata["xesam:title"] || root.artist) { | | |||
349 | return "" | | |||
350 | } | | |||
351 | | ||||
352 | var xesamUrl = (metadata["xesam:url"] || "").toString() | | |||
353 | if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" | | |||
354 | return "" | | |||
355 | } | | |||
356 | | ||||
357 | var urlParts = xesamUrl.split("/") | | |||
358 | if (urlParts.length < 3) { | | |||
359 | return "" | | |||
360 | } | | |||
361 | | ||||
362 | var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename | | |||
363 | if (lastFolderPath) { | | |||
364 | return lastFolderPath | | |||
365 | } | | |||
366 | | ||||
367 | return "" | | |||
368 | } | | |||
369 | textFormat: Text.PlainText | | |||
370 | } | | |||
371 | } | | |||
372 | | ||||
373 | Item { | | |||
374 | width: parent.width | | |||
375 | height: playerControls.height | | |||
376 | | ||||
377 | Row { | | |||
378 | id: playerControls | 400 | id: playerControls | ||
401 | | ||||
379 | property bool enabled: root.canControl | 402 | property bool enabled: root.canControl | ||
380 | property int controlsSize: theme.mSize(theme.defaultFont).height * 3 | 403 | property int controlsSize: theme.mSize(theme.defaultFont).height * 3 | ||
381 | 404 | | |||
382 | anchors.horizontalCenter: parent.horizontalCenter | 405 | Layout.alignment: Qt.AlignHCenter | ||
383 | spacing: units.largeSpacing | 406 | spacing: units.largeSpacing | ||
384 | 407 | | |||
385 | PlasmaComponents3.ToolButton { | 408 | PlasmaComponents3.ToolButton { // Previous | ||
386 | anchors.verticalCenter: parent.verticalCenter | 409 | anchors.verticalCenter: parent.verticalCenter | ||
387 | width: expandedRepresentation.controlSize | 410 | width: expandedRepresentation.controlSize | ||
388 | height: width | 411 | height: width | ||
389 | enabled: playerControls.enabled && root.canGoPrevious | 412 | enabled: playerControls.enabled && root.canGoPrevious | ||
390 | icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" | 413 | icon.name: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" | ||
391 | onClicked: { | 414 | onClicked: { | ||
392 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | 415 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | ||
393 | root.action_previous() | 416 | root.action_previous() | ||
394 | } | 417 | } | ||
395 | } | 418 | } | ||
396 | 419 | | |||
397 | PlasmaComponents3.ToolButton { | 420 | PlasmaComponents3.ToolButton { // Pause/Play | ||
398 | width: Math.round(expandedRepresentation.controlSize * 1.5) | 421 | width: Math.round(expandedRepresentation.controlSize * 1.5) | ||
399 | height: width | 422 | height: width | ||
400 | enabled: root.state == "playing" ? root.canPause : root.canPlay | 423 | enabled: root.state == "playing" ? root.canPause : root.canPlay | ||
401 | icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start" | 424 | icon.name: root.state == "playing" ? "media-playback-pause" : "media-playback-start" | ||
402 | onClicked: root.togglePlaying() | 425 | onClicked: root.togglePlaying() | ||
403 | } | 426 | } | ||
404 | 427 | | |||
405 | PlasmaComponents3.ToolButton { | 428 | PlasmaComponents3.ToolButton { // Next | ||
406 | anchors.verticalCenter: parent.verticalCenter | 429 | anchors.verticalCenter: parent.verticalCenter | ||
407 | width: expandedRepresentation.controlSize | 430 | width: expandedRepresentation.controlSize | ||
408 | height: width | 431 | height: width | ||
409 | enabled: playerControls.enabled && root.canGoNext | 432 | enabled: playerControls.enabled && root.canGoNext | ||
410 | icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" | 433 | icon.name: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" | ||
411 | onClicked: { | 434 | onClicked: { | ||
412 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | 435 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | ||
413 | root.action_next() | 436 | root.action_next() | ||
414 | } | 437 | } | ||
415 | } | 438 | } | ||
416 | } | 439 | } | ||
440 | | ||||
441 | PlasmaComponents3.ToolButton { | ||||
442 | id: playerSelector | ||||
443 | | ||||
444 | visible: tabButtonInstantiator.model.length > 2 && plasmoid.configuration.allowChangingSources // more than one player, @multiplex is always there | ||||
445 | | ||||
446 | onClicked: menu.open() | ||||
447 | | ||||
448 | text: mpris2Source.current === mpris2Source.multiplexSource ? i18n("Choose player automatically") : mpris2Source.currentData["Identity"] | ||||
449 | | ||||
450 | PlasmaComponents3.Menu { | ||||
451 | id: menu | ||||
452 | } | ||||
davidedmundson: Items in a layout shouldn't specify a width | |||||
453 | | ||||
454 | Instantiator { | ||||
455 | id: tabButtonInstantiator | ||||
456 | | ||||
457 | model: mprisSourcesModel | ||||
458 | | ||||
459 | onObjectAdded: { menu.insertItem(index, object) } | ||||
460 | onObjectRemoved: { menu.removeItem(object) } | ||||
461 | | ||||
462 | delegate: PlasmaComponents3.MenuItem { | ||||
463 | text: modelData["text"] | ||||
464 | onTriggered: { | ||||
465 | mpris2Source.current = modelData["source"] | ||||
466 | } | ||||
467 | } | ||||
468 | } | ||||
417 | } | 469 | } | ||
418 | } | 470 | } | ||
419 | 471 | | |||
420 | Timer { | 472 | Timer { | ||
421 | id: queuedPositionUpdate | 473 | id: queuedPositionUpdate | ||
422 | interval: 100 | 474 | interval: 100 | ||
423 | onTriggered: { | 475 | onTriggered: { | ||
424 | if (position == seekSlider.value) { | 476 | if (position == seekSlider.value) { | ||
Show All 9 Lines |
Superfluous since you fill both width and height?