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: 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: cntrlCol.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 { | ||
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 | } | ||
broulik: `usesPlasmaTheme: false` | |||||
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 { | ||
236 | if (metadata["xesam:title"] || root.artist) { | 189 | id: albumArt | ||
237 | return "" | 190 | anchors { | ||
191 | horizontalCenter: parent.horizontalCenter | ||||
192 | top: playerCombo.bottom | ||||
193 | bottom: cntrlCol.top | ||||
broulik: cnthrwueif..what? | |||||
194 | margins: units.smallSpacing | ||||
238 | } | 195 | } | ||
239 | 196 | | |||
240 | var xesamUrl = metadata["xesam:url"].toString() | 197 | source: root.albumArt | ||
241 | if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" | 198 | asynchronous: true | ||
242 | return "" | 199 | fillMode: Image.PreserveAspectFit | ||
200 | sourceSize: Qt.size(height, height) | ||||
201 | visible: !!root.track && status === Image.Ready | ||||
243 | } | 202 | } | ||
244 | 203 | | |||
245 | var urlParts = xesamUrl.split("/") | 204 | Column { | ||
246 | if (urlParts.length < 3) { | 205 | id: cntrlCol | ||
247 | return "" | 206 | width: parent.width | ||
248 | } | 207 | anchors.bottom: parent.bottom | ||
249 | 208 | | |||
250 | var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename | 209 | spacing: units.smallSpacing | ||
251 | if (lastFolderPath) { | | |||
252 | return lastFolderPath | | |||
253 | } | | |||
254 | 210 | | |||
255 | return "" | 211 | RowLayout { | ||
256 | } | 212 | anchors { | ||
257 | textFormat: Text.PlainText | 213 | left: parent.left | ||
258 | } | 214 | right: parent.right | ||
259 | } | 215 | margins: units.smallSpacing | ||
260 | } | 216 | } | ||
261 | 217 | | |||
262 | RowLayout { | | |||
263 | Layout.fillWidth: true | | |||
264 | spacing: units.smallSpacing | 218 | spacing: units.smallSpacing | ||
265 | 219 | | |||
266 | // if there's no "mpris:length" in the metadata, we cannot seek, so hide it in that case | 220 | // 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 | 221 | enabled: !root.noPlayer && root.track && seekSlider.maximumValue > 0 && mpris2Source.currentData.CanSeek ? true : false | ||
268 | opacity: enabled ? 1 : 0 | 222 | opacity: enabled ? 1 : 0 | ||
269 | Behavior on opacity { | 223 | Behavior on opacity { | ||
270 | NumberAnimation { duration: units.longDuration } | 224 | NumberAnimation { duration: units.longDuration } | ||
271 | } | 225 | } | ||
272 | 226 | | |||
273 | // ensure the layout doesn't shift as the numbers change and measure roughly the longest text that could occur with the current song | 227 | // 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 { | 228 | TextMetrics { | ||
275 | id: timeMetrics | 229 | id: timeMetrics | ||
276 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | 230 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | ||
277 | KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, expandedRepresentation.durationFormattingOptions)) | 231 | KCoreAddons.Format.formatDuration(seekSlider.maximumValue / 1000, expandedRepresentation.durationFormattingOptions)) | ||
278 | font: theme.smallestFont | 232 | font: theme.smallestFont | ||
279 | } | 233 | } | ||
280 | 234 | | |||
281 | PlasmaComponents.Label { | 235 | PlasmaComponents.Label { | ||
282 | Layout.preferredWidth: timeMetrics.width | 236 | Layout.preferredWidth: timeMetrics.width | ||
283 | Layout.fillHeight: true | | |||
284 | verticalAlignment: Text.AlignVCenter | 237 | verticalAlignment: Text.AlignVCenter | ||
285 | horizontalAlignment: Text.AlignRight | 238 | horizontalAlignment: Text.AlignRight | ||
286 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) | 239 | text: KCoreAddons.Format.formatDuration(seekSlider.value / 1000, expandedRepresentation.durationFormattingOptions) | ||
287 | opacity: 0.6 | 240 | opacity: 0.9 | ||
288 | font: theme.smallestFont | 241 | font: theme.smallestFont | ||
289 | } | 242 | } | ||
290 | 243 | | |||
291 | PlasmaComponents.Slider { | 244 | PlasmaComponents.Slider { | ||
292 | id: seekSlider | 245 | id: seekSlider | ||
293 | Layout.fillWidth: true | 246 | Layout.fillWidth: true | ||
294 | z: 999 | 247 | z: 999 | ||
295 | value: 0 | 248 | value: 0 | ||
Show All 23 Lines | 265 | if (!seekSlider.pressed) { | |||
319 | disablePositionUpdate = false | 272 | disablePositionUpdate = false | ||
320 | } | 273 | } | ||
321 | } | 274 | } | ||
322 | } | 275 | } | ||
323 | } | 276 | } | ||
324 | 277 | | |||
325 | PlasmaComponents.Label { | 278 | PlasmaComponents.Label { | ||
326 | Layout.preferredWidth: timeMetrics.width | 279 | Layout.preferredWidth: timeMetrics.width | ||
327 | Layout.fillHeight: true | | |||
328 | verticalAlignment: Text.AlignVCenter | 280 | verticalAlignment: Text.AlignVCenter | ||
281 | horizontalAlignment: Text.AlignLeft | ||||
329 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | 282 | text: i18nc("Remaining time for song e.g -5:42", "-%1", | ||
330 | KCoreAddons.Format.formatDuration((seekSlider.maximumValue - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) | 283 | KCoreAddons.Format.formatDuration((seekSlider.maximumValue - seekSlider.value) / 1000, expandedRepresentation.durationFormattingOptions)) | ||
331 | opacity: 0.6 | 284 | opacity: 0.9 | ||
332 | font: theme.smallestFont | 285 | font: theme.smallestFont | ||
333 | } | 286 | } | ||
334 | } | 287 | } | ||
288 | | ||||
289 | Column { | ||||
290 | width: parent.width | ||||
291 | spacing: -units.smallSpacing | ||||
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. | |||||
292 | | ||||
293 | PlasmaExtras.Heading { | ||||
294 | id: song | ||||
295 | width: parent.width | ||||
296 | level: 4 | ||||
297 | horizontalAlignment: Text.AlignHCenter | ||||
298 | | ||||
299 | maximumLineCount: 1 | ||||
300 | elide: Text.ElideRight | ||||
301 | text: { | ||||
302 | if (!root.track) | ||||
broulik: Add braces | |||||
303 | return i18n("No media playing") | ||||
304 | var artist = root.artist ? root.artist + " – " : "" | ||||
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. | |||||
305 | | ||||
306 | return artist + root.track | ||||
307 | } | ||||
308 | textFormat: Text.PlainText | ||||
335 | } | 309 | } | ||
336 | 310 | | |||
337 | Timer { | 311 | PlasmaExtras.Heading { | ||
338 | id: queuedPositionUpdate | 312 | width: parent.width | ||
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… | |||||
339 | interval: 100 | 313 | level: 7 | ||
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. | |||||
340 | onTriggered: { | 314 | opacity: 0.6 | ||
341 | if (position == seekSlider.value) { | 315 | horizontalAlignment: Text.AlignHCenter | ||
342 | return; | 316 | wrapMode: Text.NoWrap | ||
317 | elide: Text.ElideRight | ||||
318 | visible: text !== "" | ||||
319 | 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. | |||||
320 | var metadata = root.currentMetadata | ||||
321 | if (!metadata) { | ||||
322 | return "" | ||||
343 | } | 323 | } | ||
344 | var service = mpris2Source.serviceForSource(mpris2Source.current) | 324 | var xesamAlbum = metadata["xesam:album"] | ||
345 | var operation = service.operationDescription("SetPosition") | 325 | if (xesamAlbum) { | ||
346 | operation.microseconds = seekSlider.value | 326 | return xesamAlbum | ||
347 | service.startOperationCall(operation) | 327 | } | ||
328 | | ||||
329 | // if we play a local file without title and artist, show its containing folder instead | ||||
330 | if (metadata["xesam:title"] || root.artist) { | ||||
331 | return "" | ||||
332 | } | ||||
333 | | ||||
334 | var xesamUrl = metadata["xesam:url"].toString() | ||||
335 | if (xesamUrl.indexOf("file:///") !== 0) { // "!startsWith()" | ||||
336 | return "" | ||||
337 | } | ||||
338 | | ||||
339 | var urlParts = xesamUrl.split("/") | ||||
340 | if (urlParts.length < 3) { | ||||
341 | return "" | ||||
342 | } | ||||
343 | | ||||
344 | var lastFolderPath = urlParts[urlParts.length - 2] // last would be filename | ||||
345 | if (lastFolderPath) { | ||||
346 | return lastFolderPath | ||||
347 | } | ||||
348 | | ||||
349 | return "" | ||||
350 | } | ||||
351 | textFormat: Text.PlainText | ||||
348 | } | 352 | } | ||
349 | } | 353 | } | ||
350 | 354 | | |||
351 | Item { | 355 | Item { | ||
352 | anchors.bottom: parent.bottom | | |||
353 | width: parent.width | 356 | width: parent.width | ||
354 | height: playerControls.height | 357 | height: playerControls.height | ||
355 | 358 | | |||
359 | anchors.horizontalCenter: parent.horizontalCenter | ||||
broulik: Not neccessary, that item is full width already | |||||
360 | | ||||
356 | Row { | 361 | Row { | ||
357 | id: playerControls | 362 | id: playerControls | ||
358 | property bool enabled: root.canControl | 363 | property bool enabled: root.canControl | ||
359 | property int controlsSize: theme.mSize(theme.defaultFont).height * 3 | 364 | property int controlsSize: theme.mSize(theme.defaultFont).height * 3 | ||
360 | 365 | | |||
361 | anchors.horizontalCenter: parent.horizontalCenter | 366 | anchors.horizontalCenter: parent.horizontalCenter | ||
362 | spacing: units.largeSpacing | 367 | spacing: units.largeSpacing | ||
363 | 368 | | |||
Show All 26 Lines | 389 | PlasmaComponents.ToolButton { | |||
390 | onClicked: { | 395 | onClicked: { | ||
391 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | 396 | seekSlider.value = 0 // Let the media start from beginning. Bug 362473 | ||
392 | root.action_next() | 397 | root.action_next() | ||
393 | } | 398 | } | ||
394 | } | 399 | } | ||
395 | } | 400 | } | ||
396 | } | 401 | } | ||
397 | } | 402 | } | ||
403 | | ||||
404 | Timer { | ||||
405 | id: queuedPositionUpdate | ||||
406 | interval: 100 | ||||
407 | onTriggered: { | ||||
408 | if (position == seekSlider.value) { | ||||
409 | return; | ||||
410 | } | ||||
411 | var service = mpris2Source.serviceForSource(mpris2Source.current) | ||||
412 | var operation = service.operationDescription("SetPosition") | ||||
413 | operation.microseconds = seekSlider.value | ||||
414 | service.startOperationCall(operation) | ||||
415 | } | ||||
416 | } | ||||
417 | } |
Math.round