Changeset View
Changeset View
Standalone View
Standalone View
applets/mediacontroller/contents/ui/ExpandedRepresentation.qml
Show All 27 Lines | |||||
28 | Item { | 28 | Item { | ||
29 | id: expandedRepresentation | 29 | id: expandedRepresentation | ||
30 | 30 | | |||
31 | Layout.minimumWidth: Layout.minimumHeight * 1.333 | 31 | Layout.minimumWidth: Layout.minimumHeight * 1.333 | ||
32 | Layout.minimumHeight: units.gridUnit * 10 | 32 | Layout.minimumHeight: units.gridUnit * 10 | ||
33 | Layout.preferredWidth: Layout.minimumWidth * 1.5 | 33 | Layout.preferredWidth: Layout.minimumWidth * 1.5 | ||
34 | Layout.preferredHeight: Layout.minimumHeight * 1.5 | 34 | Layout.preferredHeight: Layout.minimumHeight * 1.5 | ||
35 | 35 | | |||
36 | readonly property int controlSize: Math.min(height, width) / 4 | 36 | readonly property int controlSize: units.iconSizes.large | ||
37 | 37 | | |||
38 | property int position: mpris2Source.currentData.Position || 0 | 38 | property int position: mpris2Source.currentData.Position || 0 | ||
39 | readonly property real rate: mpris2Source.currentData.Rate || 1 | 39 | readonly property real rate: mpris2Source.currentData.Rate || 1 | ||
40 | readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 | 40 | readonly property double length: currentMetadata ? currentMetadata["mpris:length"] || 0 : 0 | ||
41 | 41 | | |||
42 | // only show hours (the default for KFormat) when track is actually longer than an hour | 42 | // only show hours (the default for KFormat) when track is actually longer than an hour | ||
43 | readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours | 43 | readonly property int durationFormattingOptions: length >= 60*60*1000*1000 ? 0 : KCoreAddons.FormatTypes.FoldHours | ||
44 | 44 | | |||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Line(s) | 112 | } else if (event.key >= Qt.Key_0 && event.key <= Qt.Key_9) { | |||
113 | // jump to percentage, ie. 0 = beginnign, 1 = 10% of total length etc | 113 | // jump to percentage, ie. 0 = beginnign, 1 = 10% of total length etc | ||
114 | seekSlider.value = seekSlider.maximumValue * (event.key - Qt.Key_0) / 10 | 114 | seekSlider.value = seekSlider.maximumValue * (event.key - Qt.Key_0) / 10 | ||
115 | } else { | 115 | } else { | ||
116 | event.accepted = false | 116 | event.accepted = false | ||
117 | } | 117 | } | ||
118 | } | 118 | } | ||
119 | } | 119 | } | ||
120 | 120 | | |||
121 | ColumnLayout { | | |||
122 | id: titleColumn | | |||
123 | width: parent.width | | |||
124 | spacing: units.smallSpacing | | |||
125 | | ||||
126 | PlasmaComponents.ComboBox { | 121 | PlasmaComponents.ComboBox { | ||
127 | id: playerCombo | 122 | id: playerCombo | ||
128 | Layout.fillWidth: true | 123 | width: Math.round(0.6 * parent.width) | ||
broulik: `Math.round` | |||||
124 | height: visible ? undefined : 0 | ||||
broulik: Why is this thing no longer in a `ColumLayout`? | |||||
I want to have the controls always at the same fixed position at the bottom of the applet. With a layout it would shift them around according to the content available (which you can probably block somehow, but I couldn't really get it to work and using anchors or simple columns to achieve this seems more straightforward to me anyway). romangg: I want to have the controls always at the same fixed position at the bottom of the applet. With… | |||||
125 | anchors.horizontalCenter: parent.horizontalCenter | ||||
129 | visible: model.length > 2 // more than one player, @multiplex is always there | 126 | visible: model.length > 2 // more than one player, @multiplex is always there | ||
130 | model: { | 127 | model: { | ||
131 | var model = [{ | 128 | var model = [{ | ||
132 | text: i18n("Choose player automatically"), | 129 | text: i18n("Choose player automatically"), | ||
133 | source: mpris2Source.multiplexSource | 130 | source: mpris2Source.multiplexSource | ||
134 | }] | 131 | }] | ||
135 | 132 | | |||
136 | var sources = mpris2Source.sources | 133 | var sources = mpris2Source.sources | ||
Show All 24 Lines | |||||
161 | onActivated: { | 158 | onActivated: { | ||
162 | disablePositionUpdate = true | 159 | disablePositionUpdate = true | ||
163 | // ComboBox has currentIndex and currentText, why doesn't it have currentItem/currentModelValue? | 160 | // ComboBox has currentIndex and currentText, why doesn't it have currentItem/currentModelValue? | ||
164 | mpris2Source.current = model[index].source | 161 | mpris2Source.current = model[index].source | ||
165 | disablePositionUpdate = false | 162 | disablePositionUpdate = false | ||
166 | } | 163 | } | ||
167 | } | 164 | } | ||
168 | 165 | | |||
169 | RowLayout { | 166 | Item { | ||
170 | id: titleRow | 167 | anchors { | ||
171 | Layout.fillWidth: true | 168 | horizontalCenter: parent.horizontalCenter | ||
172 | Layout.minimumHeight: albumArt.Layout.preferredHeight | 169 | top: playerCombo.bottom | ||
173 | spacing: units.largeSpacing | 170 | bottom: controlCol.top | ||
174 | 171 | margins: units.smallSpacing | |||
175 | Image { | | |||
176 | id: albumArt | | |||
177 | readonly property int size: Math.round(expandedRepresentation.height / 2 - (playerCombo.count > 2 ? playerCombo.height : 0)) | | |||
178 | source: root.albumArt | | |||
179 | asynchronous: true | | |||
180 | fillMode: Image.PreserveAspectCrop | | |||
181 | sourceSize: Qt.size(size, size) | | |||
182 | Layout.preferredHeight: size | | |||
183 | Layout.preferredWidth: size | | |||
184 | visible: !!root.track && status === Image.Ready | | |||
185 | } | 172 | } | ||
186 | 173 | | |||
187 | ColumnLayout { | 174 | PlasmaCore.IconItem { | ||
broulik: `usesPlasmaTheme: false` | |||||
188 | Layout.fillWidth: true | 175 | anchors { | ||
189 | spacing: units.smallSpacing / 2 | 176 | horizontalCenter: parent.horizontalCenter | ||
190 | 177 | verticalCenter: parent.verticalCenter | |||
191 | PlasmaExtras.Heading { | | |||
192 | id: song | | |||
193 | Layout.fillWidth: true | | |||
194 | level: 3 | | |||
195 | opacity: 0.6 | | |||
196 | | ||||
197 | maximumLineCount: 3 | | |||
198 | wrapMode: Text.WrapAtWordBoundaryOrAnywhere | | |||
199 | elide: Text.ElideRight | | |||
200 | text: root.track ? root.track : i18n("No media playing") | | |||
201 | textFormat: Text.PlainText | | |||
202 | } | 178 | } | ||
203 | 179 | | |||
204 | PlasmaExtras.Heading { | 180 | height: parent.height / 2 | ||
205 | id: artist | 181 | width: height | ||
206 | Layout.fillWidth: true | | |||
207 | level: 4 | | |||
208 | opacity: 0.4 | | |||
209 | maximumLineCount: 2 | | |||
210 | wrapMode: Text.WrapAtWordBoundaryOrAnywhere | | |||
211 | visible: text !== "" | | |||
212 | 182 | | |||
213 | elide: Text.ElideRight | 183 | source: mpris2Source.currentData["Desktop Icon Name"] | ||
214 | text: root.artist || "" | 184 | visible: !albumArt.visible | ||
215 | textFormat: Text.PlainText | | |||
216 | } | | |||
217 | | ||||
218 | PlasmaExtras.Heading { | | |||
219 | Layout.fillWidth: true | | |||
220 | level: 5 | | |||
221 | opacity: 0.4 | | |||
222 | wrapMode: Text.NoWrap | | |||
223 | elide: Text.ElideRight | | |||
224 | visible: text !== "" | | |||
225 | text: { | | |||
226 | var metadata = root.currentMetadata | | |||
227 | if (!metadata) { | | |||
228 | return "" | | |||
229 | } | 185 | } | ||
230 | var xesamAlbum = metadata["xesam:album"] | | |||
231 | if (xesamAlbum) { | | |||
232 | return xesamAlbum | | |||
233 | } | 186 | } | ||
234 | 187 | | |||
235 | // if we play a local file without title and artist, show its containing folder instead | 188 | Image { | ||
broulik: cnthrwueif..what? | |||||
236 | if (metadata["xesam:title"] || root.artist) { | 189 | id: albumArt | ||
237 | return "" | 190 | anchors { | ||
191 | horizontalCenter: parent.horizontalCenter | ||||
192 | top: playerCombo.bottom | ||||
193 | bottom: controlCol.top | ||||
194 | margins: units.smallSpacing | ||||
238 | } | 195 | } | ||
239 | 196 | source: root.albumArt | |||
240 | var xesamUrl = (metadata["xesam:url"] || "").toString() | 197 | asynchronous: true | ||
241 | if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" | 198 | fillMode: Image.PreserveAspectFit | ||
242 | return "" | 199 | sourceSize: Qt.size(height, height) | ||
200 | visible: !!root.track && status === Image.Ready | ||||
243 | } | 201 | } | ||
244 | 202 | | |||
245 | var urlParts = xesamUrl.split("/") | 203 | Column { | ||
246 | if (urlParts.length < 3) { | 204 | id: controlCol | ||
247 | return "" | 205 | width: parent.width | ||
248 | } | 206 | anchors.bottom: parent.bottom | ||
249 | 207 | | |||
250 | var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename | 208 | spacing: units.smallSpacing | ||
251 | if (lastFolderPath) { | | |||
252 | return lastFolderPath | | |||
253 | } | | |||
254 | 209 | | |||
255 | return "" | 210 | RowLayout { | ||
256 | } | 211 | anchors { | ||
257 | textFormat: Text.PlainText | 212 | left: parent.left | ||
258 | } | 213 | right: parent.right | ||
259 | } | 214 | margins: units.smallSpacing | ||
260 | } | 215 | } | ||
261 | 216 | | |||
262 | RowLayout { | | |||
263 | Layout.fillWidth: true | | |||
264 | spacing: units.smallSpacing | 217 | spacing: units.smallSpacing | ||
265 | 218 | | |||
266 | // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case | 219 | // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case | ||
267 | enabled: !root.noPlayer && root.track && seekSlider.maximumValue > 0 && mpris2Source.currentData.CanSeek ? true : false | 220 | enabled: !root.noPlayer && root.track && seekSlider.maximumValue > 0 && mpris2Source.currentData.CanSeek ? true : false | ||
268 | opacity: enabled ? 1 : 0 | 221 | opacity: enabled ? 1 : 0 | ||
269 | Behavior on opacity { | 222 | Behavior on opacity { | ||
270 | NumberAnimation { duration: units.longDuration } | 223 | NumberAnimation { duration: units.longDuration } | ||
271 | } | 224 | } | ||
272 | 225 | | |||
273 | // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song | 226 | // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song | ||
274 | TextMetrics { | 227 | TextMetrics { | ||
275 | id: timeMetrics | 228 | id: timeMetrics | ||
276 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | 229 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | ||
277 | KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, expandedRepresentation.durationFormattingOptions)) | 230 | KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, expandedRepresentation.durationFormattingOptions)) | ||
278 | font: theme.smallestFont | 231 | font: theme.smallestFont | ||
279 | } | 232 | } | ||
280 | 233 | | |||
281 | PlasmaComponents.Label { | 234 | PlasmaComponents.Label { | ||
282 | Layout.preferredWidth: timeMetrics.width | 235 | Layout.preferredWidth: timeMetrics.width | ||
283 | Layout.fillHeight: true | | |||
284 | verticalAlignment: Text.AlignVCenter | 236 | verticalAlignment: Text.AlignVCenter | ||
285 | horizontalAlignment: Text.AlignRight | 237 | horizontalAlignment: Text.AlignRight | ||
286 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) | 238 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) | ||
287 | opacity: 0.6 | 239 | opacity: 0.9 | ||
288 | font: theme.smallestFont | 240 | font: theme.smallestFont | ||
289 | } | 241 | } | ||
290 | 242 | | |||
291 | PlasmaComponents.Slider { | 243 | PlasmaComponents.Slider { | ||
292 | id: seekSlider | 244 | id: seekSlider | ||
293 | Layout.fillWidth: true | 245 | Layout.fillWidth: true | ||
294 | z: 999 | 246 | z: 999 | ||
295 | value: 0 | 247 | value: 0 | ||
Show All 23 Lines | 264 | if (!seekSlider.pressed) { | |||
319 | disablePositionUpdate = false | 271 | disablePositionUpdate = false | ||
320 | } | 272 | } | ||
321 | } | 273 | } | ||
322 | } | 274 | } | ||
323 | } | 275 | } | ||
324 | 276 | | |||
325 | PlasmaComponents.Label { | 277 | PlasmaComponents.Label { | ||
326 | Layout.preferredWidth: timeMetrics.width | 278 | Layout.preferredWidth: timeMetrics.width | ||
327 | Layout.fillHeight: true | | |||
328 | verticalAlignment: Text.AlignVCenter | 279 | verticalAlignment: Text.AlignVCenter | ||
280 | horizontalAlignment: Text.AlignLeft | ||||
329 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | 281 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | ||
330 | KCoreAddons.Format.formatDuration((seekSlider.maximumValue - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) | 282 | KCoreAddons.Format.formatDuration((seekSlider.maximumValue - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) | ||
331 | opacity: 0.6 | 283 | opacity: 0.9 | ||
332 | font: theme.smallestFont | 284 | font: theme.smallestFont | ||
333 | } | 285 | } | ||
334 | } | 286 | } | ||
287 | | ||||
288 | Column { | ||||
289 | width: parent.width | ||||
290 | | ||||
romangg: Pls tell me a way to use Headings without huge margins all around. | |||||
broulik: `height: undefined` | |||||
romangg: If I forget this trick one more time, hit me. | |||||
291 | PlasmaExtras.Heading { | ||||
292 | id: song | ||||
293 | width: parent.width | ||||
294 | height: undefined | ||||
295 | level: 4 | ||||
296 | horizontalAlignment: Text.AlignHCenter | ||||
297 | | ||||
298 | maximumLineCount: 1 | ||||
299 | elide: Text.ElideRight | ||||
300 | text: { | ||||
301 | if (!root.track) { | ||||
broulik: Add braces | |||||
302 | return i18n("No media playing") | ||||
303 | } | ||||
return root.artist ? i18nc("artist - track", "%1 – %2", root.artist, root.track) : root.track broulik: ```return root.artist ? i18nc("artist - track", "%1 – %2", root.artist, root.track) : root. | |||||
304 | return root.artist ? i18nc("artist – track", "%1 – %2", root.artist, root.track) : root.track | ||||
305 | } | ||||
306 | textFormat: Text.PlainText | ||||
335 | } | 307 | } | ||
336 | 308 | | |||
337 | Timer { | 309 | PlasmaExtras.Heading { | ||
338 | id: queuedPositionUpdate | 310 | width: parent.width | ||
339 | interval: 100 | 311 | height: undefined | ||
broulik: Why not use a `ColumnLayout`? | |||||
As above. I want to have this stuff always at fixed positions. Or is there a simple way to achieve this with Layouts as well? romangg: As above. I want to have this stuff always at fixed positions. Or is there a simple way to… | |||||
340 | onTriggered: { | 312 | level: 5 | ||
broulik: I thought level only went up to 5 but fine with me | |||||
romangg: Yea, you're right. It just interprets everything above 5 as 5. | |||||
341 | if (position == seekSlider.value) { | 313 | opacity: 0.6 | ||
342 | return; | 314 | horizontalAlignment: Text.AlignHCenter | ||
315 | wrapMode: Text.NoWrap | ||||
316 | elide: Text.ElideRight | ||||
317 | visible: text !== "" | ||||
318 | text: { | ||||
So you now use the file name as "album"? How does it behave when you have no id3 info at all? broulik: So you now use the file name as "album"? How does it behave when you have no id3 info at all? | |||||
romangg: I don't have changed anything in this regard. | |||||
319 | var metadata = root.currentMetadata | ||||
320 | if (!metadata) { | ||||
321 | return "" | ||||
343 | } | 322 | } | ||
344 | var service = mpris2Source.serviceForSource(mpris2Source.current) | 323 | var xesamAlbum = metadata["xesam:album"] | ||
345 | var operation = service.operationDescription("SetPosition") | 324 | if (xesamAlbum) { | ||
346 | operation.microseconds = seekSlider.value | 325 | return xesamAlbum | ||
347 | service.startOperationCall(operation) | 326 | } | ||
327 | | ||||
328 | // if we play a local file without title and artist, show its containing folder instead | ||||
329 | if (metadata["xesam:title"] || root.artist) { | ||||
330 | return "" | ||||
331 | } | ||||
332 | | ||||
333 | var xesamUrl = (metadata["xesam:url"] || "").toString() | ||||
334 | if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" | ||||
335 | return "" | ||||
336 | } | ||||
337 | | ||||
338 | var urlParts = xesamUrl.split("/") | ||||
339 | if (urlParts.length < 3) { | ||||
340 | return "" | ||||
341 | } | ||||
342 | | ||||
343 | var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename | ||||
344 | if (lastFolderPath) { | ||||
345 | return lastFolderPath | ||||
346 | } | ||||
347 | | ||||
348 | return "" | ||||
349 | } | ||||
350 | textFormat: Text.PlainText | ||||
348 | } | 351 | } | ||
349 | } | 352 | } | ||
350 | 353 | | |||
351 | Item { | 354 | Item { | ||
352 | anchors.bottom: parent.bottom | | |||
353 | width: parent.width | 355 | width: parent.width | ||
354 | height: playerControls.height | 356 | height: playerControls.height | ||
355 | 357 | | |||
356 | Row { | 358 | Row { | ||
broulik: Not neccessary, that item is full width already | |||||
357 | id: playerControls | 359 | id: playerControls | ||
358 | property bool enabled: root.canControl | 360 | property bool enabled: root.canControl | ||
359 | property int controlsSize: theme.mSize(theme.defaultFont).height * 3 | 361 | property int controlsSize: theme.mSize(theme.defaultFont).height * 3 | ||
360 | 362 | | |||
361 | anchors.horizontalCenter: parent.horizontalCenter | 363 | anchors.horizontalCenter: parent.horizontalCenter | ||
362 | spacing: units.largeSpacing | 364 | spacing: units.largeSpacing | ||
363 | 365 | | |||
364 | PlasmaComponents.ToolButton { | 366 | PlasmaComponents.ToolButton { | ||
365 | anchors.verticalCenter: parent.verticalCenter | 367 | anchors.verticalCenter: parent.verticalCenter | ||
366 | width: expandedRepresentation.controlSize | 368 | width: expandedRepresentation.controlSize | ||
367 | height: width | 369 | height: width | ||
368 | enabled: playerControls.enabled && root.canGoPrevious | 370 | enabled: playerControls.enabled && root.canGoPrevious | ||
369 | iconSource: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" | 371 | iconSource: LayoutMirroring.enabled ? "media-skip-forward" : "media-skip-backward" | ||
370 | onClicked: { | 372 | onClicked: { | ||
371 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | 373 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | ||
372 | root.action_previous() | 374 | root.action_previous() | ||
373 | } | 375 | } | ||
374 | } | 376 | } | ||
375 | 377 | | |||
376 | PlasmaComponents.ToolButton { | 378 | PlasmaComponents.ToolButton { | ||
377 | width: Math.round(expandedRepresentation.controlSize * 1.5) | 379 | width: Math.round(expandedRepresentation.controlSize * 1.5) | ||
378 | height: width | 380 | height: width | ||
379 | enabled: root.state == "playing" ? root.canPause : root.canPlay | 381 | enabled: root.state == "playing" ? root.canPause : root.canPlay | ||
380 | iconSource: root.state == "playing" ? "media-playback-pause" : "media-playback-start" | 382 | iconSource: root.state == "playing" ? "media-playback-pause" : "media-playback-start" | ||
381 | onClicked: root.togglePlaying(); | 383 | onClicked: root.togglePlaying() | ||
382 | } | 384 | } | ||
383 | 385 | | |||
384 | PlasmaComponents.ToolButton { | 386 | PlasmaComponents.ToolButton { | ||
385 | anchors.verticalCenter: parent.verticalCenter | 387 | anchors.verticalCenter: parent.verticalCenter | ||
386 | width: expandedRepresentation.controlSize | 388 | width: expandedRepresentation.controlSize | ||
387 | height: width | 389 | height: width | ||
388 | enabled: playerControls.enabled && root.canGoNext | 390 | enabled: playerControls.enabled && root.canGoNext | ||
389 | iconSource: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" | 391 | iconSource: LayoutMirroring.enabled ? "media-skip-backward" : "media-skip-forward" | ||
390 | onClicked: { | 392 | onClicked: { | ||
391 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | 393 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | ||
392 | root.action_next() | 394 | root.action_next() | ||
393 | } | 395 | } | ||
394 | } | 396 | } | ||
395 | } | 397 | } | ||
396 | } | 398 | } | ||
397 | } | 399 | } | ||
400 | | ||||
401 | Timer { | ||||
402 | id: queuedPositionUpdate | ||||
403 | interval: 100 | ||||
404 | onTriggered: { | ||||
405 | if (position == seekSlider.value) { | ||||
406 | return; | ||||
407 | } | ||||
408 | var service = mpris2Source.serviceForSource(mpris2Source.current) | ||||
409 | var operation = service.operationDescription("SetPosition") | ||||
410 | operation.microseconds = seekSlider.value | ||||
411 | service.startOperationCall(operation) | ||||
412 | } | ||||
413 | } | ||||
414 | } |
Math.round