/** * SPDX-FileCopyrightText: 2021-2023 Bart De Vries * SPDX-FileCopyrightText: 2021 Devin Lin * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ import QtQuick import QtQuick.Controls as Controls import QtQuick.Layouts import QtQuick.Effects import org.kde.kirigami as Kirigami import org.kde.kmediasession import org.kde.kasts import org.kde.kasts.settings import ".." Kirigami.Page { id: playerControls title: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Loaded") clip: true Layout.margins: 0 leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: Kirigami.Units.gridUnit Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.inherit: false Component { id: slider ChapterSlider { model: chapterModel } } background: MultiEffect { source: backgroundImage anchors.fill: parent opacity: 0.2 brightness: 0.3 saturation: 2 contrast: -0.7 blurMax: 64 blur: 1.0 blurEnabled: true autoPaddingEnabled: false Image { id: backgroundImage source: AudioManager.entry.cachedImage asynchronous: true visible: false anchors.fill: parent fillMode: Image.PreserveAspectCrop } } ColumnLayout { id: playerControlsColumn anchors.fill: parent anchors.topMargin: Kirigami.Units.largeSpacing * 2 anchors.bottomMargin: Kirigami.Units.largeSpacing spacing: 0 Controls.Button { id: swipeUpButton property int swipeUpButtonSize: Kirigami.Units.iconSizes.smallMedium icon.name: "arrow-down" icon.height: swipeUpButtonSize icon.width: swipeUpButtonSize flat: true Layout.alignment: Qt.AlignHCenter Layout.topMargin: 0 Layout.bottomMargin: Kirigami.Units.largeSpacing onClicked: toClose.restart() } Controls.SwipeView { id: swipeView currentIndex: 0 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.preferredWidth: parent.width Layout.preferredHeight: parent.height - media.height - swipeUpButton.height Layout.margins: 0 // we are unable to use Controls.Control here to set padding since it seems to eat touch events on the parent flickable Item { Item { property int textMargin: Kirigami.Units.largeSpacing // margin above the text below the image anchors.fill: parent anchors.leftMargin: Kirigami.Units.largeSpacing * 2 anchors.rightMargin: Kirigami.Units.largeSpacing * 2 Item { id: coverImage anchors { top: parent.top bottom: kastsMainWindow.isWidescreen ? parent.bottom : undefined left: parent.left right: kastsMainWindow.isWidescreen ? undefined : parent.right margins: 0 topMargin: kastsMainWindow.isWidescreen ? 0 : (parent.height - Math.min(height, width) - imageLabels.implicitHeight - 2 * parent.textMargin) / 2 } height: Math.min(parent.height - (kastsMainWindow.isWidescreen ? 0 : imageLabels.implicitHeight + 2 * parent.textMargin), parent.width) width: kastsMainWindow.isWidescreen ? Math.min(parent.height, parent.width / 2) : Math.min(parent.width, height) ImageWithFallback { imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image" imageFillMode: Image.PreserveAspectCrop anchors.centerIn: parent anchors.margins: 0 width: Math.min(parent.width, parent.height) height: Math.min(parent.height, parent.width) fractionalRadius: 1 / 20 } } Item { anchors { top: kastsMainWindow.isWidescreen ? parent.top : coverImage.bottom bottom: parent.bottom left: kastsMainWindow.isWidescreen ? coverImage.right : parent.left right: parent.right leftMargin: kastsMainWindow.isWidescreen ? parent.textMargin : 0 topMargin: kastsMainWindow.isWidescreen ? 0 : parent.textMargin bottomMargin: 0 } ColumnLayout { id: imageLabels anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: parent.right anchors.margins: 0 Controls.Label { text: AudioManager.entry ? AudioManager.entry.title : i18n("No Title") elide: Text.ElideRight Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width font.weight: Font.Medium } Controls.Label { text: AudioManager.entry ? AudioManager.entry.feed.name : i18n("No podcast title") elide: Text.ElideRight Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width opacity: 0.6 } } } } } Item { Flickable { anchors.fill: parent anchors.leftMargin: Kirigami.Units.largeSpacing * 2 + playerControlsColumn.anchors.margins anchors.rightMargin: Kirigami.Units.largeSpacing * 2 clip: true contentHeight: description.height ColumnLayout { id: description width: parent.width Kirigami.Heading { text: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Title") level: 3 wrapMode: Text.WordWrap Layout.fillWidth: true Layout.bottomMargin: Kirigami.Units.largeSpacing } Controls.Label { id: text Layout.fillWidth: true text: AudioManager.entry ? AudioManager.entry.adjustedContent(width, font.pixelSize) : i18n("No track loaded") verticalAlignment: Text.AlignTop baseUrl: AudioManager.entry ? AudioManager.entry.baseUrl : "" textFormat: Text.RichText wrapMode: Text.WordWrap onLinkHovered: { cursorShape: Qt.PointingHandCursor; } onLinkActivated: (link) => { if (link.split("://")[0] === "timestamp") { if (AudioManager.entry && AudioManager.entry.enclosure) { AudioManager.seek(link.split("://")[1]); } } else { Qt.openUrlExternally(link); } } } } } } Item { Item { anchors.fill: parent anchors.leftMargin: Kirigami.Units.largeSpacing * 2 anchors.rightMargin: Kirigami.Units.largeSpacing * 2 Kirigami.PlaceholderMessage { visible: chapterList.count === 0 width: parent.width anchors.centerIn: parent text: i18n("No chapters found") } ListView { id: chapterList model: ChapterModel { id: chapterModel entry: AudioManager.entry ? AudioManager.entry : null duration: AudioManager.duration / 1000 } clip: true visible: chapterList.count !== 0 anchors.fill: parent delegate: ChapterListDelegate { entry: AudioManager.entry ? AudioManager.entry : null } } } } } Item { id: media implicitHeight: mediaControls.height Layout.leftMargin: Kirigami.Units.largeSpacing * 2 Layout.rightMargin: Kirigami.Units.largeSpacing * 2 Layout.bottomMargin: 0 Layout.topMargin: 0 Layout.fillWidth: true ColumnLayout { id: mediaControls anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 0 spacing: 0 RowLayout { id: contextButtons Layout.preferredHeight: Kirigami.Units.gridUnit * 3 Layout.leftMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing Layout.topMargin: Kirigami.Units.smallSpacing Layout.bottomMargin: Kirigami.Units.smallSpacing Layout.fillWidth: true property int iconSize: Math.floor(Kirigami.Units.gridUnit * 1.3) property int buttonSize: bottomRow.buttonSize Controls.ToolButton { visible: AudioManager.entry checked: swipeView.currentIndex === 0 Layout.maximumHeight: parent.height Layout.preferredHeight: contextButtons.buttonSize Layout.maximumWidth: height Layout.preferredWidth: height icon.name: "viewimage" icon.width: contextButtons.iconSize icon.height: contextButtons.iconSize onClicked: { swipeView.currentIndex = 0; } } Controls.ToolButton { visible: AudioManager.entry checked: swipeView.currentIndex === 1 Layout.maximumHeight: parent.height Layout.preferredHeight: contextButtons.buttonSize Layout.maximumWidth: height Layout.preferredWidth: height icon.name: "documentinfo" icon.width: contextButtons.iconSize icon.height: contextButtons.iconSize onClicked: { swipeView.currentIndex = 1; } } Controls.ToolButton { visible: AudioManager.entry && chapterList.count !== 0 checked: swipeView.currentIndex === 2 Layout.maximumHeight: parent.height Layout.preferredHeight: contextButtons.buttonSize Layout.maximumWidth: height Layout.preferredWidth: height icon.name: "view-media-playlist" icon.width: contextButtons.iconSize icon.height: contextButtons.iconSize onClicked: { swipeView.currentIndex = 2; } } Item { Layout.fillWidth: true } Controls.ToolButton { checkable: true checked: AudioManager.remainingSleepTime > 0 Layout.maximumHeight: parent.height Layout.preferredHeight: contextButtons.buttonSize Layout.maximumWidth: height Layout.preferredWidth: height icon.name: "clock" icon.width: contextButtons.iconSize icon.height: contextButtons.iconSize onClicked: { toggle(); // only set the on/off state based on sleep timer state sleepTimerDialog.open() } } Controls.ToolButton { id: volumeButton Layout.maximumHeight: parent.height Layout.preferredHeight: contextButtons.buttonSize Layout.maximumWidth: height Layout.preferredWidth: height icon.name: AudioManager.muted ? "player-volume-muted" : "player-volume" icon.width: contextButtons.iconSize icon.height: contextButtons.iconSize enabled: AudioManager.PlaybackState != AudioManager.StoppedState && AudioManager.canPlay checked: volumePopup.visible Controls.ToolTip.visible: hovered Controls.ToolTip.delay: Kirigami.Units.toolTipDelay Controls.ToolTip.text: i18nc("@action:button", "Open volume settings") onClicked: { if (volumePopup.visible) { volumePopup.close(); } else { volumePopup.open(); } } Controls.Popup { id: volumePopup x: -padding y: -volumePopup.height focus: true padding: Kirigami.Units.smallSpacing contentWidth: muteButton.implicitWidth contentItem: ColumnLayout { id: popupContent Controls.ToolButton { id: muteButton enabled: AudioManager.PlaybackState != AudioManager.StoppedState && AudioManager.canPlay icon.name: AudioManager.muted ? "player-volume-muted" : "player-volume" onClicked: AudioManager.muted = !AudioManager.muted Controls.ToolTip.visible: hovered Controls.ToolTip.delay: Kirigami.Units.toolTipDelay Controls.ToolTip.text: i18nc("@action:button", "Toggle mute") } VolumeSlider { id: volumeSlider } } } } } Loader { active: !kastsMainWindow.isWidescreen visible: !kastsMainWindow.isWidescreen sourceComponent: slider Layout.fillWidth: true Layout.leftMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing } RowLayout { id: controls Layout.leftMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true Controls.Label { Layout.alignment: Qt.AlignLeft padding: Kirigami.Units.largeSpacing text: AudioManager.formattedPosition font: Kirigami.Theme.smallFont } Loader { active: kastsMainWindow.isWidescreen sourceComponent: slider Layout.fillWidth: true } Item { Layout.alignment: Qt.AlignRight Layout.preferredHeight: endLabel.implicitHeight Layout.preferredWidth: endLabel.implicitWidth Controls.Label { id: endLabel padding: Kirigami.Units.largeSpacing anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: (SettingsManager.toggleRemainingTime) ? "-" + AudioManager.formattedLeftDuration : AudioManager.formattedDuration font: Kirigami.Theme.smallFont } MouseArea { anchors.fill: parent hoverEnabled: true onClicked: { SettingsManager.toggleRemainingTime = !SettingsManager.toggleRemainingTime; SettingsManager.save(); } } } } RowLayout { id: bottomRow Layout.leftMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing Layout.maximumWidth: Number.POSITIVE_INFINITY //TODO ? Layout.fillWidth: true Layout.bottomMargin: 0 Layout.topMargin: 0 // Make button width scale properly on narrow windows instead of overflowing property int buttonSize: Math.min(playButton.implicitWidth, ((playerControlsColumn.width - 4 * spacing) / 5 - playButton.leftPadding - playButton.rightPadding)) property int iconSize: Kirigami.Units.gridUnit * 1.5 // left section Controls.Button { id: playbackRateButton text: AudioManager.playbackRate.toFixed(2) + "x" checkable: true checked: playbackRateMenu.visible onClicked: { if (playbackRateMenu.visible) { playbackRateMenu.dismiss(); } else { playbackRateMenu.popup(playbackRateButton, 0, -playbackRateMenu.height); } } flat: true padding: 0 implicitWidth: playButton.width * 1.5 implicitHeight: playButton.height } // middle section RowLayout { spacing: Kirigami.Units.largeSpacing Layout.alignment: Qt.AlignHCenter Controls.Button { icon.name: "media-seek-backward" icon.height: bottomRow.iconSize icon.width: bottomRow.iconSize flat: true Layout.alignment: Qt.AlignRight Layout.preferredWidth: bottomRow.buttonSize onClicked: AudioManager.skipBackward() enabled: AudioManager.canSkipBackward } Controls.Button { id: playButton icon.name: AudioManager.playbackState === KMediaSession.PlayingState ? "media-playback-pause" : "media-playback-start" icon.height: bottomRow.iconSize icon.width: bottomRow.iconSize flat: true Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: bottomRow.buttonSize onClicked: AudioManager.playbackState === KMediaSession.PlayingState ? AudioManager.pause() : AudioManager.play() enabled: AudioManager.canPlay } Controls.Button { icon.name: "media-seek-forward" icon.height: bottomRow.iconSize icon.width: bottomRow.iconSize flat: true Layout.alignment: Qt.AlignLeft Layout.preferredWidth: bottomRow.buttonSize onClicked: AudioManager.skipForward() enabled: AudioManager.canSkipForward } } // right section Controls.Button { icon.name: "media-skip-forward" icon.height: bottomRow.iconSize icon.width: bottomRow.iconSize flat: true Layout.alignment: Qt.AlignRight Layout.preferredWidth: bottomRow.buttonSize onClicked: AudioManager.next() enabled: AudioManager.canGoNext } } } } } PlaybackRateMenu { id: playbackRateMenu parentButton: playbackRateButton } }