diff --git a/src/audiomanager.h b/src/audiomanager.h index 3d7e881b..40dda060 100644 --- a/src/audiomanager.h +++ b/src/audiomanager.h @@ -38,7 +38,11 @@ class AudioManager : public QObject Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged) public: - explicit AudioManager(QObject *parent = nullptr); + static AudioManager &instance() + { + static AudioManager _instance; + return _instance; + } ~AudioManager() override; @@ -163,6 +167,8 @@ private Q_SLOTS: void savePlayPosition(qint64 position); private: + explicit AudioManager(QObject *parent = nullptr); + friend class AudioManagerPrivate; std::unique_ptr d; diff --git a/src/main.cpp b/src/main.cpp index ef016df7..3b5707f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,7 +107,6 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.kasts", 1, 0, "FeedsModel"); qmlRegisterType("org.kde.kasts", 1, 0, "QueueModel"); qmlRegisterType("org.kde.kasts", 1, 0, "EpisodeModel"); - qmlRegisterType("org.kde.kasts", 1, 0, "AudioManager"); qmlRegisterType("org.kde.kasts", 1, 0, "Mpris2"); qmlRegisterUncreatableType("org.kde.kasts", 1, 0, "EntriesModel", QStringLiteral("Get from Feed")); @@ -119,6 +118,8 @@ int main(int argc, char *argv[]) qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "SettingsManager", SettingsManager::self()); qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "DownloadProgressModel", &DownloadProgressModel::instance()); qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "ErrorLogModel", &ErrorLogModel::instance()); + qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "AudioManager", &AudioManager::instance()); + qRegisterMetaType("const Entry*"); // "hack" to make qml understand Entry* diff --git a/src/qml/EntryPage.qml b/src/qml/EntryPage.qml index 404ea9e9..a7d8ee19 100644 --- a/src/qml/EntryPage.qml +++ b/src/qml/EntryPage.qml @@ -72,13 +72,13 @@ Kirigami.ScrollablePage { entry.enclosure.status === Enclosure.Downloadable ? i18n("Download") : entry.enclosure.status === Enclosure.Downloading ? i18n("Cancel download") : !entry.queueStatus ? i18n("Delete download") : - (audio.entry === entry && audio.playbackState === Audio.PlayingState) ? i18n("Pause") : + (AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState) ? i18n("Pause") : i18n("Play") icon.name: !entry.enclosure ? "globe" : entry.enclosure.status === Enclosure.Downloadable ? "download" : entry.enclosure.status === Enclosure.Downloading ? "edit-delete-remove" : !entry.queueStatus ? "delete" : - (audio.entry === entry && audio.playbackState === Audio.PlayingState) ? "media-playback-pause" : + (AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState) ? "media-playback-pause" : "media-playback-start" onTriggered: { if(!entry.enclosure) Qt.openUrlExternally(entry.link) @@ -87,11 +87,11 @@ Kirigami.ScrollablePage { else if(!entry.queueStatus) { entry.enclosure.deleteFile() } else { - if(audio.entry === entry && audio.playbackState === Audio.PlayingState) { - audio.pause() + if(AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState) { + AudioManager.pause() } else { - audio.entry = entry - audio.play() + AudioManager.entry = entry + AudioManager.play() } } } @@ -106,8 +106,8 @@ Kirigami.ScrollablePage { entry.queueStatus = true } else { // first change to next track if this one is playing - if (entry.hasEnclosure && entry === audio.entry) { - audio.next() + if (entry.hasEnclosure && entry === AudioManager.entry) { + AudioManager.next() } entry.queueStatus = false } diff --git a/src/qml/GenericEntryDelegate.qml b/src/qml/GenericEntryDelegate.qml index 54425de8..efc9cae6 100644 --- a/src/qml/GenericEntryDelegate.qml +++ b/src/qml/GenericEntryDelegate.qml @@ -97,7 +97,7 @@ Kirigami.SwipeListItem { Component { id: subtitle Controls.Label { - text: audio.timeString(entry.enclosure.duration * 1000) + text: AudioManager.timeString(entry.enclosure.duration * 1000) Layout.fillWidth: true elide: Text.ElideRight font: Kirigami.Theme.smallFont @@ -119,7 +119,7 @@ Kirigami.SwipeListItem { id: playProgress RowLayout { Controls.Label { - text: audio.timeString(entry.enclosure.playPosition) + text: AudioManager.timeString(entry.enclosure.playPosition) elide: Text.ElideRight font: Kirigami.Theme.smallFont opacity: entry.read ? 0.4 : 0.7 @@ -132,7 +132,7 @@ Kirigami.SwipeListItem { opacity: entry.read ? 0.6 : 1 } Controls.Label { - text: audio.timeString(entry.enclosure.duration * 1000) + text: AudioManager.timeString(entry.enclosure.duration * 1000) elide: Text.ElideRight font: Kirigami.Theme.smallFont opacity: entry.read ? 0.4 : 0.7 @@ -195,17 +195,17 @@ Kirigami.SwipeListItem { Kirigami.Action { text: i18n("Play") icon.name: "media-playback-start" - visible: !isDownloads && entry.queueStatus && entry.enclosure && entry.enclosure.status === Enclosure.Downloaded && (audio.entry !== entry || audio.playbackState !== Audio.PlayingState) + visible: !isDownloads && entry.queueStatus && entry.enclosure && entry.enclosure.status === Enclosure.Downloaded && (AudioManager.entry !== entry || AudioManager.playbackState !== Audio.PlayingState) onTriggered: { - audio.entry = entry - audio.play() + AudioManager.entry = entry + AudioManager.play() } }, Kirigami.Action { text: i18n("Pause") icon.name: "media-playback-pause" - visible: !isDownloads && entry.queueStatus && entry.enclosure && entry.enclosure.status === Enclosure.Downloaded && audio.entry === entry && audio.playbackState === Audio.PlayingState - onTriggered: audio.pause() + visible: !isDownloads && entry.queueStatus && entry.enclosure && entry.enclosure.status === Enclosure.Downloaded && AudioManager.entry === entry && AudioManager.playbackState === Audio.PlayingState + onTriggered: AudioManager.pause() } ] } diff --git a/src/qml/MinimizedPlayerControls.qml b/src/qml/MinimizedPlayerControls.qml index 2c4fa082..d451c878 100644 --- a/src/qml/MinimizedPlayerControls.qml +++ b/src/qml/MinimizedPlayerControls.qml @@ -20,7 +20,7 @@ Item { property int buttonsize: Kirigami.Units.gridUnit * 2 height: miniplayerheight + progressbarheight - visible: audio.entry + visible: AudioManager.entry // Set background Rectangle { @@ -36,7 +36,7 @@ Item { anchors.left: parent.left height: parent.progressbarheight color: Kirigami.Theme.highlightColor - width: parent.width * audio.position / audio.duration + width: parent.width * AudioManager.position / AudioManager.duration visible: true } @@ -52,7 +52,7 @@ Item { anchors.fill: parent ImageWithFallback { - imageSource: audio.entry.cachedImage + imageSource: AudioManager.entry.cachedImage Layout.preferredHeight: miniplayerheight Layout.preferredWidth: miniplayerheight } @@ -64,7 +64,7 @@ Item { Layout.leftMargin: Kirigami.Units.smallSpacing Controls.Label { id: mainLabel - text: audio.entry.title + text: AudioManager.entry.title wrapMode: Text.Wrap Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.fillWidth: true @@ -77,7 +77,7 @@ Item { Controls.Label { id: feedLabel - text: audio.entry.feed.name + text: AudioManager.entry.feed.name wrapMode: Text.Wrap Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.fillWidth: true @@ -98,14 +98,14 @@ Item { } Controls.Button { id: playButton - icon.name: audio.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start" + icon.name: AudioManager.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start" icon.height: parent.parent.buttonsize icon.width: parent.parent.buttonsize flat: true Layout.fillHeight: true Layout.maximumHeight: parent.parent.miniplayerheight Layout.maximumWidth: height - onClicked: audio.playbackState === Audio.PlayingState ? audio.pause() : audio.play() + onClicked: AudioManager.playbackState === Audio.PlayingState ? AudioManager.pause() : AudioManager.play() Layout.alignment: Qt.AlignVCenter } } diff --git a/src/qml/PlayerControls.qml b/src/qml/PlayerControls.qml index 66875fcb..fe01ea86 100644 --- a/src/qml/PlayerControls.qml +++ b/src/qml/PlayerControls.qml @@ -16,7 +16,7 @@ import org.kde.kasts 1.0 Kirigami.Page { id: playerControls - title: audio.entry ? audio.entry.title : "No track loaded" + title: AudioManager.entry ? AudioManager.entry.title : "No track loaded" clip: true Layout.margins: 0 @@ -50,7 +50,7 @@ Kirigami.Page { property int textMargin: Kirigami.Units.gridUnit // margin above and below the text below the image ImageWithFallback { id: coverImage - imageSource: audio.entry ? audio.entry.cachedImage : "no-image" + imageSource: AudioManager.entry ? AudioManager.entry.cachedImage : "no-image" imageFillMode: Image.PreserveAspectCrop anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top @@ -67,13 +67,13 @@ Kirigami.Page { anchors.right: parent.right anchors.topMargin: parent.textMargin Controls.Label { - text: audio.entry ? audio.entry.title : "No title" + text: AudioManager.entry ? AudioManager.entry.title : "No title" elide: Text.ElideRight Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width } Controls.Label { - text: audio.entry ? audio.entry.feed.name : "No feed" + text: AudioManager.entry ? AudioManager.entry.feed.name : "No feed" elide: Text.ElideRight Layout.alignment: Qt.AlignHCenter Layout.maximumWidth: parent.width @@ -90,7 +90,7 @@ Kirigami.Page { id: description width: parent.width Kirigami.Heading { - text: audio.entry ? audio.entry.title : "No track title" + text: AudioManager.entry ? AudioManager.entry.title : "No track title" level: 3 wrapMode: Text.WordWrap Layout.fillWidth: true @@ -98,9 +98,9 @@ Kirigami.Page { } Controls.Label { id: text - text: audio.entry ? audio.entry.content : "No track loaded" + text: AudioManager.entry ? AudioManager.entry.content : "No track loaded" verticalAlignment: Text.AlignTop - baseUrl: audio.entry ? audio.entry.baseUrl : "" + baseUrl: AudioManager.entry ? AudioManager.entry.baseUrl : "" textFormat: Text.RichText wrapMode: Text.WordWrap onLinkActivated: Qt.openUrlExternally(link) @@ -135,14 +135,14 @@ Kirigami.Page { anchors.margins: 0 Controls.Slider { - enabled: audio.entry + enabled: AudioManager.entry Layout.fillWidth: true Layout.margins: 0 padding: 0 from: 0 - to: audio.duration - value: audio.position - onMoved: audio.seek(value) + to: AudioManager.duration + value: AudioManager.position + onMoved: AudioManager.seek(value) } RowLayout { id: controls @@ -150,7 +150,7 @@ Kirigami.Page { Layout.fillWidth: true Controls.Label { padding: Kirigami.Units.largeSpacing - text: audio.timeString(audio.position) + text: AudioManager.timeString(AudioManager.position) } Item { Layout.fillWidth: true @@ -164,8 +164,8 @@ Kirigami.Page { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: (SettingsManager.toggleRemainingTime) ? - "-" + audio.timeString(audio.duration-audio.position) - : audio.timeString(audio.duration) + "-" + AudioManager.timeString(AudioManager.duration-AudioManager.position) + : AudioManager.timeString(AudioManager.duration) } MouseArea { @@ -189,15 +189,15 @@ Kirigami.Page { // Use contentItem and a Label because using plain "text" // does not rescale automatically if the text changes contentItem: Controls.Label { - text: audio.playbackRate + "x" + text: AudioManager.playbackRate + "x" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } onClicked: { - if(audio.playbackRate === 2.5) - audio.playbackRate = 1 + if(AudioManager.playbackRate === 2.5) + AudioManager.playbackRate = 1 else - audio.playbackRate = audio.playbackRate + 0.25 + AudioManager.playbackRate = AudioManager.playbackRate + 0.25 } flat: true Layout.alignment: Qt.AlignHCenter @@ -212,19 +212,19 @@ Kirigami.Page { flat: true Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: parent.buttonSize - onClicked: audio.skipBackward() - enabled: audio.canSkipBackward + onClicked: AudioManager.skipBackward() + enabled: AudioManager.canSkipBackward } Controls.Button { id: playButton - icon.name: audio.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start" + icon.name: AudioManager.playbackState === Audio.PlayingState ? "media-playback-pause" : "media-playback-start" icon.height: parent.iconSize icon.width: parent.iconSize flat: true Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: parent.buttonSize - onClicked: audio.playbackState === Audio.PlayingState ? audio.pause() : audio.play() - enabled: audio.canPlay + onClicked: AudioManager.playbackState === Audio.PlayingState ? AudioManager.pause() : AudioManager.play() + enabled: AudioManager.canPlay } Controls.Button { icon.name: "media-seek-forward" @@ -233,8 +233,8 @@ Kirigami.Page { flat: true Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: parent.buttonSize - onClicked: audio.skipForward() - enabled: audio.canSkipForward + onClicked: AudioManager.skipForward() + enabled: AudioManager.canSkipForward } Controls.Button { icon.name: "media-skip-forward" @@ -243,8 +243,8 @@ Kirigami.Page { flat: true Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: parent.buttonSize - onClicked: audio.next() - enabled: audio.canGoNext + onClicked: AudioManager.next() + enabled: AudioManager.canGoNext } } } diff --git a/src/qml/QueuePage.qml b/src/qml/QueuePage.qml index dc752402..90a4ad1a 100644 --- a/src/qml/QueuePage.qml +++ b/src/qml/QueuePage.qml @@ -61,7 +61,7 @@ Kirigami.ScrollablePage { Controls.Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter - text: i18np("1 episode", "%1 episodes", queueModel.rowCount()) + " · " + i18n("Time left") + ": " + audio.timeString(queueModel.timeLeft) + text: i18np("1 episode", "%1 episodes", queueModel.rowCount()) + " · " + i18n("Time left") + ": " + AudioManager.timeString(queueModel.timeLeft) } Kirigami.Separator { Layout.fillWidth: true @@ -70,7 +70,6 @@ Kirigami.ScrollablePage { model: QueueModel { id: queueModel - audioManager: audio } delegate: Kirigami.DelegateRecycler { diff --git a/src/qml/main.qml b/src/qml/main.qml index 91a0179e..fdbfa408 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -21,7 +21,7 @@ Kirigami.ApplicationWindow { property var miniplayerSize: Kirigami.Units.gridUnit * 3 + Kirigami.Units.gridUnit / 6 property int tabBarHeight: Kirigami.Units.gridUnit * 2 - property int bottomMessageSpacing: Kirigami.Units.largeSpacing * 9 + ( audio.entry ? ( footerLoader.item.contentY == 0 ? miniplayerSize : 0 ) : 0 ) + tabBarActive * tabBarHeight + property int bottomMessageSpacing: Kirigami.Units.largeSpacing * 9 + ( AudioManager.entry ? ( footerLoader.item.contentY == 0 ? miniplayerSize : 0 ) : 0 ) + tabBarActive * tabBarHeight property int tabBarActive: 0 Kirigami.PagePool { @@ -46,8 +46,8 @@ Kirigami.ApplicationWindow { globalDrawer: Kirigami.GlobalDrawer { isMenu: false // make room at the bottom for miniplayer - handle.anchors.bottomMargin: (audio.entry ? (footerLoader.item.contentY == 0 ? miniplayerSize : 0) : 0) + Kirigami.Units.smallSpacing + tabBarActive * tabBarHeight - handleVisible: !audio.entry || footerLoader.item.contentY === 0 + handle.anchors.bottomMargin: (AudioManager.entry ? (footerLoader.item.contentY == 0 ? miniplayerSize : 0) : 0) + Kirigami.Units.smallSpacing + tabBarActive * tabBarHeight + handleVisible: !AudioManager.entry || footerLoader.item.contentY === 0 actions: [ Kirigami.PagePoolAction { text: i18n("Queue") @@ -117,19 +117,15 @@ Kirigami.ApplicationWindow { contextDrawer: Kirigami.ContextDrawer { id: contextDrawer // make room at the bottom for miniplayer - handle.anchors.bottomMargin: ( audio.entry ? ( footerLoader.item.contentY == 0 ? miniplayerSize : 0 ) : 0 ) + Kirigami.Units.smallSpacing + tabBarActive * tabBarHeight - handleVisible: !audio.entry || footerLoader.item.contentY == 0 - } - - AudioManager { - id: audio + handle.anchors.bottomMargin: ( AudioManager.entry ? ( footerLoader.item.contentY == 0 ? miniplayerSize : 0 ) : 0 ) + Kirigami.Units.smallSpacing + tabBarActive * tabBarHeight + handleVisible: !AudioManager.entry || footerLoader.item.contentY == 0 } Mpris2 { id: mpris2Interface playerName: 'kasts' - audioPlayer: audio + audioPlayer: AudioManager onRaisePlayer: { @@ -139,13 +135,13 @@ Kirigami.ApplicationWindow { // create space at the bottom to show miniplayer without it hiding stuff // underneath - pageStack.anchors.bottomMargin: (audio.entry) ? miniplayerSize : 0 + pageStack.anchors.bottomMargin: (AudioManager.entry) ? miniplayerSize : 0 Loader { id: footerLoader anchors.fill: parent - active: audio.entry + active: AudioManager.entry visible: active z: (!item || item.contentY === 0) ? -1 : 999 sourceComponent: FooterBar { diff --git a/src/queuemodel.cpp b/src/queuemodel.cpp index e83de137..65073f22 100644 --- a/src/queuemodel.cpp +++ b/src/queuemodel.cpp @@ -35,6 +35,12 @@ QueueModel::QueueModel(QObject *parent) Q_EMIT timeLeftChanged(); // qDebug() << "Removed entry at pos" << pos; }); + // Connect positionChanged to make sure that the remaining playing time in + // the queue header is up-to-date + connect(&AudioManager::instance(), &AudioManager::positionChanged, this, [this](qint64 position) { + Q_UNUSED(position) + Q_EMIT timeLeftChanged(); + }); } QVariant QueueModel::data(const QModelIndex &index, int role) const @@ -72,19 +78,3 @@ int QueueModel::timeLeft() const // qDebug() << "timeLeft is" << result; return result; } - -AudioManager *QueueModel::audioManager() -{ - return m_audio; -} - -void QueueModel::setAudioManager(AudioManager *audio) -{ - // AudioManager is qml-owned; we need the pointer to the instance - // in order to connect to the positionChanged signal - m_audio = audio; - connect(m_audio, &AudioManager::positionChanged, this, [this](qint64 position) { - Q_UNUSED(position) - Q_EMIT timeLeftChanged(); - }); -} diff --git a/src/queuemodel.h b/src/queuemodel.h index f2901c0b..3e67b3ff 100644 --- a/src/queuemodel.h +++ b/src/queuemodel.h @@ -18,7 +18,6 @@ class QueueModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(int timeLeft READ timeLeft NOTIFY timeLeftChanged) - Q_PROPERTY(AudioManager *audioManager READ audioManager WRITE setAudioManager) public: static QueueModel &instance() @@ -33,9 +32,6 @@ public: int rowCount(const QModelIndex &parent) const override; int timeLeft() const; - AudioManager *audioManager(); - void setAudioManager(AudioManager *audio); - Q_SIGNALS: void timeLeftChanged();