Warning, /multimedia/kasts/src/qml/Mobile/MobilePlayerControls.qml is written in an unsupported language. File is not indexed.
0001 /**
0002 * SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
0003 * SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
0004 *
0005 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007
0008 import QtQuick
0009 import QtQuick.Controls as Controls
0010 import QtQuick.Layouts
0011 import QtQuick.Effects
0012
0013 import org.kde.kirigami as Kirigami
0014 import org.kde.kmediasession
0015
0016 import org.kde.kasts
0017 import org.kde.kasts.settings
0018
0019 import ".."
0020
0021 Kirigami.Page {
0022 id: playerControls
0023
0024 title: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Loaded")
0025 clip: true
0026 Layout.margins: 0
0027
0028 leftPadding: 0
0029 rightPadding: 0
0030 topPadding: 0
0031 bottomPadding: Kirigami.Units.gridUnit
0032
0033 Kirigami.Theme.colorSet: Kirigami.Theme.View
0034 Kirigami.Theme.inherit: false
0035
0036 Component {
0037 id: slider
0038 ChapterSlider {
0039 model: chapterModel
0040 }
0041 }
0042
0043 background: MultiEffect {
0044 source: backgroundImage
0045 anchors.fill: parent
0046 opacity: 0.2
0047
0048 brightness: 0.3
0049 saturation: 2
0050 contrast: -0.7
0051 blurMax: 64
0052 blur: 1.0
0053 blurEnabled: true
0054 autoPaddingEnabled: false
0055
0056 Image {
0057 id: backgroundImage
0058 source: AudioManager.entry.cachedImage
0059 asynchronous: true
0060 visible: false
0061 anchors.fill: parent
0062 fillMode: Image.PreserveAspectCrop
0063 }
0064 }
0065
0066 ColumnLayout {
0067 id: playerControlsColumn
0068 anchors.fill: parent
0069 anchors.topMargin: Kirigami.Units.largeSpacing * 2
0070 anchors.bottomMargin: Kirigami.Units.largeSpacing
0071 spacing: 0
0072
0073 Controls.Button {
0074 id: swipeUpButton
0075 property int swipeUpButtonSize: Kirigami.Units.iconSizes.smallMedium
0076 icon.name: "arrow-down"
0077 icon.height: swipeUpButtonSize
0078 icon.width: swipeUpButtonSize
0079 flat: true
0080 Layout.alignment: Qt.AlignHCenter
0081 Layout.topMargin: 0
0082 Layout.bottomMargin: Kirigami.Units.largeSpacing
0083 onClicked: toClose.restart()
0084 }
0085
0086 Controls.SwipeView {
0087 id: swipeView
0088
0089 currentIndex: 0
0090 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
0091 Layout.preferredWidth: parent.width
0092 Layout.preferredHeight: parent.height - media.height - swipeUpButton.height
0093 Layout.margins: 0
0094
0095 // we are unable to use Controls.Control here to set padding since it seems to eat touch events on the parent flickable
0096 Item {
0097 Item {
0098 property int textMargin: Kirigami.Units.largeSpacing // margin above the text below the image
0099 anchors.fill: parent
0100 anchors.leftMargin: Kirigami.Units.largeSpacing * 2
0101 anchors.rightMargin: Kirigami.Units.largeSpacing * 2
0102
0103 Item {
0104 id: coverImage
0105 anchors {
0106 top: parent.top
0107 bottom: kastsMainWindow.isWidescreen ? parent.bottom : undefined
0108 left: parent.left
0109 right: kastsMainWindow.isWidescreen ? undefined : parent.right
0110 margins: 0
0111 topMargin: kastsMainWindow.isWidescreen ? 0 : (parent.height - Math.min(height, width) - imageLabels.implicitHeight - 2 * parent.textMargin) / 2
0112 }
0113 height: Math.min(parent.height - (kastsMainWindow.isWidescreen ? 0 : imageLabels.implicitHeight + 2 * parent.textMargin), parent.width)
0114 width: kastsMainWindow.isWidescreen ? Math.min(parent.height, parent.width / 2) : Math.min(parent.width, height)
0115
0116 ImageWithFallback {
0117 imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image"
0118 imageFillMode: Image.PreserveAspectCrop
0119 anchors.centerIn: parent
0120 anchors.margins: 0
0121 width: Math.min(parent.width, parent.height)
0122 height: Math.min(parent.height, parent.width)
0123 fractionalRadius: 1 / 20
0124 }
0125 }
0126
0127 Item {
0128 anchors {
0129 top: kastsMainWindow.isWidescreen ? parent.top : coverImage.bottom
0130 bottom: parent.bottom
0131 left: kastsMainWindow.isWidescreen ? coverImage.right : parent.left
0132 right: parent.right
0133 leftMargin: kastsMainWindow.isWidescreen ? parent.textMargin : 0
0134 topMargin: kastsMainWindow.isWidescreen ? 0 : parent.textMargin
0135 bottomMargin: 0
0136 }
0137
0138 ColumnLayout {
0139 id: imageLabels
0140 anchors.verticalCenter: parent.verticalCenter
0141 anchors.left: parent.left
0142 anchors.right: parent.right
0143 anchors.margins: 0
0144 Controls.Label {
0145 text: AudioManager.entry ? AudioManager.entry.title : i18n("No Title")
0146 elide: Text.ElideRight
0147 Layout.alignment: Qt.AlignHCenter
0148 Layout.maximumWidth: parent.width
0149 font.weight: Font.Medium
0150 }
0151
0152 Controls.Label {
0153 text: AudioManager.entry ? AudioManager.entry.feed.name : i18n("No podcast title")
0154 elide: Text.ElideRight
0155 Layout.alignment: Qt.AlignHCenter
0156 Layout.maximumWidth: parent.width
0157 opacity: 0.6
0158 }
0159 }
0160 }
0161 }
0162 }
0163
0164 Item {
0165 Flickable {
0166 anchors.fill: parent
0167 anchors.leftMargin: Kirigami.Units.largeSpacing * 2 + playerControlsColumn.anchors.margins
0168 anchors.rightMargin: Kirigami.Units.largeSpacing * 2
0169 clip: true
0170 contentHeight: description.height
0171 ColumnLayout {
0172 id: description
0173 width: parent.width
0174 Kirigami.Heading {
0175 text: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Title")
0176 level: 3
0177 wrapMode: Text.WordWrap
0178 Layout.fillWidth: true
0179 Layout.bottomMargin: Kirigami.Units.largeSpacing
0180 }
0181
0182 Controls.Label {
0183 id: text
0184 Layout.fillWidth: true
0185 text: AudioManager.entry ? AudioManager.entry.adjustedContent(width, font.pixelSize) : i18n("No track loaded")
0186 verticalAlignment: Text.AlignTop
0187 baseUrl: AudioManager.entry ? AudioManager.entry.baseUrl : ""
0188 textFormat: Text.RichText
0189 wrapMode: Text.WordWrap
0190 onLinkHovered: {
0191 cursorShape: Qt.PointingHandCursor;
0192 }
0193 onLinkActivated: (link) => {
0194 if (link.split("://")[0] === "timestamp") {
0195 if (AudioManager.entry && AudioManager.entry.enclosure) {
0196 AudioManager.seek(link.split("://")[1]);
0197 }
0198 } else {
0199 Qt.openUrlExternally(link);
0200 }
0201 }
0202 }
0203 }
0204 }
0205 }
0206
0207 Item {
0208 Item {
0209 anchors.fill: parent
0210 anchors.leftMargin: Kirigami.Units.largeSpacing * 2
0211 anchors.rightMargin: Kirigami.Units.largeSpacing * 2
0212
0213 Kirigami.PlaceholderMessage {
0214 visible: chapterList.count === 0
0215
0216 width: parent.width
0217 anchors.centerIn: parent
0218
0219 text: i18n("No chapters found")
0220 }
0221
0222 ListView {
0223 id: chapterList
0224 model: ChapterModel {
0225 id: chapterModel
0226 entry: AudioManager.entry ? AudioManager.entry : null
0227 duration: AudioManager.duration / 1000
0228 }
0229 clip: true
0230 visible: chapterList.count !== 0
0231 anchors.fill: parent
0232 delegate: ChapterListDelegate {
0233 entry: AudioManager.entry ? AudioManager.entry : null
0234 }
0235 }
0236 }
0237 }
0238 }
0239
0240 Item {
0241 id: media
0242
0243 implicitHeight: mediaControls.height
0244 Layout.leftMargin: Kirigami.Units.largeSpacing * 2
0245 Layout.rightMargin: Kirigami.Units.largeSpacing * 2
0246 Layout.bottomMargin: 0
0247 Layout.topMargin: 0
0248 Layout.fillWidth: true
0249
0250 ColumnLayout {
0251 id: mediaControls
0252
0253 anchors.left: parent.left
0254 anchors.right: parent.right
0255 anchors.bottom: parent.bottom
0256 anchors.margins: 0
0257 spacing: 0
0258
0259 RowLayout {
0260 id: contextButtons
0261 Layout.preferredHeight: Kirigami.Units.gridUnit * 3
0262 Layout.leftMargin: Kirigami.Units.largeSpacing
0263 Layout.rightMargin: Kirigami.Units.largeSpacing
0264 Layout.topMargin: Kirigami.Units.smallSpacing
0265 Layout.bottomMargin: Kirigami.Units.smallSpacing
0266 Layout.fillWidth: true
0267 property int iconSize: Math.floor(Kirigami.Units.gridUnit * 1.3)
0268 property int buttonSize: bottomRow.buttonSize
0269
0270 Controls.ToolButton {
0271 visible: AudioManager.entry
0272 checked: swipeView.currentIndex === 0
0273 Layout.maximumHeight: parent.height
0274 Layout.preferredHeight: contextButtons.buttonSize
0275 Layout.maximumWidth: height
0276 Layout.preferredWidth: height
0277 icon.name: "viewimage"
0278 icon.width: contextButtons.iconSize
0279 icon.height: contextButtons.iconSize
0280 onClicked: {
0281 swipeView.currentIndex = 0;
0282 }
0283 }
0284
0285 Controls.ToolButton {
0286 visible: AudioManager.entry
0287 checked: swipeView.currentIndex === 1
0288 Layout.maximumHeight: parent.height
0289 Layout.preferredHeight: contextButtons.buttonSize
0290 Layout.maximumWidth: height
0291 Layout.preferredWidth: height
0292 icon.name: "documentinfo"
0293 icon.width: contextButtons.iconSize
0294 icon.height: contextButtons.iconSize
0295 onClicked: {
0296 swipeView.currentIndex = 1;
0297 }
0298 }
0299
0300 Controls.ToolButton {
0301 visible: AudioManager.entry && chapterList.count !== 0
0302 checked: swipeView.currentIndex === 2
0303 Layout.maximumHeight: parent.height
0304 Layout.preferredHeight: contextButtons.buttonSize
0305 Layout.maximumWidth: height
0306 Layout.preferredWidth: height
0307 icon.name: "view-media-playlist"
0308 icon.width: contextButtons.iconSize
0309 icon.height: contextButtons.iconSize
0310 onClicked: {
0311 swipeView.currentIndex = 2;
0312 }
0313 }
0314
0315 Item {
0316 Layout.fillWidth: true
0317 }
0318
0319 Controls.ToolButton {
0320 checkable: true
0321 checked: AudioManager.remainingSleepTime > 0
0322 Layout.maximumHeight: parent.height
0323 Layout.preferredHeight: contextButtons.buttonSize
0324 Layout.maximumWidth: height
0325 Layout.preferredWidth: height
0326 icon.name: "clock"
0327 icon.width: contextButtons.iconSize
0328 icon.height: contextButtons.iconSize
0329 onClicked: {
0330 toggle(); // only set the on/off state based on sleep timer state
0331 sleepTimerDialog.open()
0332 }
0333 }
0334 Controls.ToolButton {
0335 id: volumeButton
0336 Layout.maximumHeight: parent.height
0337 Layout.preferredHeight: contextButtons.buttonSize
0338 Layout.maximumWidth: height
0339 Layout.preferredWidth: height
0340 icon.name: AudioManager.muted ? "player-volume-muted" : "player-volume"
0341 icon.width: contextButtons.iconSize
0342 icon.height: contextButtons.iconSize
0343 enabled: AudioManager.PlaybackState != AudioManager.StoppedState && AudioManager.canPlay
0344 checked: volumePopup.visible
0345
0346 Controls.ToolTip.visible: hovered
0347 Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
0348 Controls.ToolTip.text: i18nc("@action:button", "Open volume settings")
0349
0350 onClicked: {
0351 if (volumePopup.visible) {
0352 volumePopup.close();
0353 } else {
0354 volumePopup.open();
0355 }
0356 }
0357
0358 Controls.Popup {
0359 id: volumePopup
0360 x: -padding
0361 y: -volumePopup.height
0362
0363 focus: true
0364 padding: Kirigami.Units.smallSpacing
0365 contentWidth: muteButton.implicitWidth
0366
0367 contentItem: ColumnLayout {
0368 id: popupContent
0369
0370 Controls.ToolButton {
0371 id: muteButton
0372 enabled: AudioManager.PlaybackState != AudioManager.StoppedState && AudioManager.canPlay
0373 icon.name: AudioManager.muted ? "player-volume-muted" : "player-volume"
0374 onClicked: AudioManager.muted = !AudioManager.muted
0375
0376 Controls.ToolTip.visible: hovered
0377 Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
0378 Controls.ToolTip.text: i18nc("@action:button", "Toggle mute")
0379
0380 }
0381
0382 VolumeSlider {
0383 id: volumeSlider
0384 }
0385 }
0386 }
0387 }
0388 }
0389
0390 Loader {
0391 active: !kastsMainWindow.isWidescreen
0392 visible: !kastsMainWindow.isWidescreen
0393 sourceComponent: slider
0394 Layout.fillWidth: true
0395 Layout.leftMargin: Kirigami.Units.largeSpacing
0396 Layout.rightMargin: Kirigami.Units.largeSpacing
0397 }
0398
0399 RowLayout {
0400 id: controls
0401 Layout.leftMargin: Kirigami.Units.largeSpacing
0402 Layout.rightMargin: Kirigami.Units.largeSpacing
0403 Layout.fillWidth: true
0404 Controls.Label {
0405 Layout.alignment: Qt.AlignLeft
0406 padding: Kirigami.Units.largeSpacing
0407 text: AudioManager.formattedPosition
0408 font: Kirigami.Theme.smallFont
0409 }
0410 Loader {
0411 active: kastsMainWindow.isWidescreen
0412 sourceComponent: slider
0413 Layout.fillWidth: true
0414
0415 }
0416 Item {
0417 Layout.alignment: Qt.AlignRight
0418 Layout.preferredHeight: endLabel.implicitHeight
0419 Layout.preferredWidth: endLabel.implicitWidth
0420 Controls.Label {
0421 id: endLabel
0422 padding: Kirigami.Units.largeSpacing
0423 anchors.right: parent.right
0424 anchors.verticalCenter: parent.verticalCenter
0425 text: (SettingsManager.toggleRemainingTime) ?
0426 "-" + AudioManager.formattedLeftDuration
0427 : AudioManager.formattedDuration
0428 font: Kirigami.Theme.smallFont
0429
0430 }
0431 MouseArea {
0432 anchors.fill: parent
0433 hoverEnabled: true
0434 onClicked: {
0435 SettingsManager.toggleRemainingTime = !SettingsManager.toggleRemainingTime;
0436 SettingsManager.save();
0437 }
0438 }
0439 }
0440 }
0441
0442 RowLayout {
0443 id: bottomRow
0444 Layout.leftMargin: Kirigami.Units.largeSpacing
0445 Layout.rightMargin: Kirigami.Units.largeSpacing
0446 Layout.maximumWidth: Number.POSITIVE_INFINITY //TODO ?
0447 Layout.fillWidth: true
0448 Layout.bottomMargin: 0
0449 Layout.topMargin: 0
0450
0451 // Make button width scale properly on narrow windows instead of overflowing
0452 property int buttonSize: Math.min(playButton.implicitWidth, ((playerControlsColumn.width - 4 * spacing) / 5 - playButton.leftPadding - playButton.rightPadding))
0453 property int iconSize: Kirigami.Units.gridUnit * 1.5
0454
0455 // left section
0456 Controls.Button {
0457 id: playbackRateButton
0458 text: AudioManager.playbackRate.toFixed(2) + "x"
0459 checkable: true
0460 checked: playbackRateMenu.visible
0461 onClicked: {
0462 if (playbackRateMenu.visible) {
0463 playbackRateMenu.dismiss();
0464 } else {
0465 playbackRateMenu.popup(playbackRateButton, 0, -playbackRateMenu.height);
0466 }
0467 }
0468 flat: true
0469 padding: 0
0470 implicitWidth: playButton.width * 1.5
0471 implicitHeight: playButton.height
0472 }
0473
0474 // middle section
0475 RowLayout {
0476 spacing: Kirigami.Units.largeSpacing
0477 Layout.alignment: Qt.AlignHCenter
0478 Controls.Button {
0479 icon.name: "media-seek-backward"
0480 icon.height: bottomRow.iconSize
0481 icon.width: bottomRow.iconSize
0482 flat: true
0483 Layout.alignment: Qt.AlignRight
0484 Layout.preferredWidth: bottomRow.buttonSize
0485 onClicked: AudioManager.skipBackward()
0486 enabled: AudioManager.canSkipBackward
0487 }
0488 Controls.Button {
0489 id: playButton
0490 icon.name: AudioManager.playbackState === KMediaSession.PlayingState ? "media-playback-pause" : "media-playback-start"
0491 icon.height: bottomRow.iconSize
0492 icon.width: bottomRow.iconSize
0493 flat: true
0494 Layout.alignment: Qt.AlignHCenter
0495 Layout.preferredWidth: bottomRow.buttonSize
0496 onClicked: AudioManager.playbackState === KMediaSession.PlayingState ? AudioManager.pause() : AudioManager.play()
0497 enabled: AudioManager.canPlay
0498 }
0499 Controls.Button {
0500 icon.name: "media-seek-forward"
0501 icon.height: bottomRow.iconSize
0502 icon.width: bottomRow.iconSize
0503 flat: true
0504 Layout.alignment: Qt.AlignLeft
0505 Layout.preferredWidth: bottomRow.buttonSize
0506 onClicked: AudioManager.skipForward()
0507 enabled: AudioManager.canSkipForward
0508 }
0509 }
0510
0511 // right section
0512 Controls.Button {
0513 icon.name: "media-skip-forward"
0514 icon.height: bottomRow.iconSize
0515 icon.width: bottomRow.iconSize
0516 flat: true
0517 Layout.alignment: Qt.AlignRight
0518 Layout.preferredWidth: bottomRow.buttonSize
0519 onClicked: AudioManager.next()
0520 enabled: AudioManager.canGoNext
0521 }
0522 }
0523 }
0524 }
0525 }
0526
0527 PlaybackRateMenu {
0528 id: playbackRateMenu
0529 parentButton: playbackRateButton
0530 }
0531 }