diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c68b7917..14d23bae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -222,6 +222,7 @@ if(ANDROID) emblem-music-symbolic gpodder kaccounts-nextcloud + clock ) else() target_link_libraries(kasts PRIVATE Qt::Widgets Qt::DBus) diff --git a/src/audiomanager.cpp b/src/audiomanager.cpp index 8bff3c19..abc065c0 100644 --- a/src/audiomanager.cpp +++ b/src/audiomanager.cpp @@ -44,6 +44,10 @@ private: // still pending qint64 m_pendingSeek = -1; + QTimer *m_sleepTimer = nullptr; + qint64 m_sleepTime = -1; + qint64 m_remainingSleepTime = -1; + friend class AudioManager; }; @@ -600,3 +604,73 @@ QString AudioManager::formattedPosition() const { return m_kformat.formatDuration(position()); } + +qint64 AudioManager::sleepTime() const +{ + if (d->m_sleepTimer) { + return d->m_sleepTime; + } else { + return -1; + } +} + +qint64 AudioManager::remainingSleepTime() const +{ + if (d->m_sleepTimer) { + return d->m_remainingSleepTime; + } else { + return -1; + } +} + +void AudioManager::setSleepTimer(qint64 duration) +{ + if (duration > 0) { + if (d->m_sleepTimer) { + stopSleepTimer(); + } + + d->m_sleepTime = duration; + d->m_remainingSleepTime = duration; + + d->m_sleepTimer = new QTimer(this); + connect(d->m_sleepTimer, &QTimer::timeout, this, [this]() { + (d->m_remainingSleepTime)--; + if (d->m_remainingSleepTime > 0) { + Q_EMIT remainingSleepTimeChanged(remainingSleepTime()); + } else { + pause(); + stopSleepTimer(); + } + }); + d->m_sleepTimer->start(1000); + + Q_EMIT sleepTimerChanged(duration); + Q_EMIT remainingSleepTimeChanged(remainingSleepTime()); + } else { + stopSleepTimer(); + } +} + +void AudioManager::stopSleepTimer() +{ + if (d->m_sleepTimer) { + d->m_sleepTime = -1; + d->m_remainingSleepTime = -1; + + delete d->m_sleepTimer; + d->m_sleepTimer = nullptr; + + Q_EMIT sleepTimerChanged(-1); + Q_EMIT remainingSleepTimeChanged(-1); + } +} + +QString AudioManager::formattedRemainingSleepTime() const +{ + qint64 timeLeft = remainingSleepTime() * 1000; + if (timeLeft < 0) { + timeLeft = 0; + } + return m_kformat.formatDuration(timeLeft); +} diff --git a/src/audiomanager.h b/src/audiomanager.h index 56cd6d9f..edbcf77b 100644 --- a/src/audiomanager.h +++ b/src/audiomanager.h @@ -42,6 +42,9 @@ class AudioManager : public QObject Q_PROPERTY(QString formattedLeftDuration READ formattedLeftDuration NOTIFY positionChanged) Q_PROPERTY(QString formattedDuration READ formattedDuration NOTIFY durationChanged) Q_PROPERTY(QString formattedPosition READ formattedPosition NOTIFY positionChanged) + Q_PROPERTY(qint64 sleepTime READ sleepTime WRITE setSleepTimer RESET stopSleepTimer NOTIFY sleepTimerChanged) + Q_PROPERTY(qint64 remainingSleepTime READ remainingSleepTime NOTIFY remainingSleepTimeChanged) + Q_PROPERTY(QString formattedRemainingSleepTime READ formattedRemainingSleepTime NOTIFY remainingSleepTimeChanged) public: const double MAX_RATE = 1.0; @@ -58,133 +61,88 @@ public: ~AudioManager() override; [[nodiscard]] Entry *entry() const; - [[nodiscard]] bool muted() const; - [[nodiscard]] qreal volume() const; - [[nodiscard]] QUrl source() const; - [[nodiscard]] QMediaPlayer::MediaStatus status() const; - [[nodiscard]] QMediaPlayer::State playbackState() const; - [[nodiscard]] qreal playbackRate() const; - [[nodiscard]] qreal minimumPlaybackRate() const; - [[nodiscard]] qreal maximumPlaybackRate() const; - [[nodiscard]] QMediaPlayer::Error error() const; - [[nodiscard]] qint64 duration() const; - [[nodiscard]] qint64 position() const; - [[nodiscard]] bool seekable() const; - [[nodiscard]] bool canPlay() const; - [[nodiscard]] bool canPause() const; - [[nodiscard]] bool canSkipForward() const; - [[nodiscard]] bool canSkipBackward() const; - [[nodiscard]] bool canGoNext() const; QString formattedDuration() const; QString formattedLeftDuration() const; QString formattedPosition() const; + qint64 sleepTime() const; // returns originally set sleep time + qint64 remainingSleepTime() const; // returns remaining sleep time + QString formattedRemainingSleepTime() const; + Q_SIGNALS: void entryChanged(Entry *entry); - void mutedChanged(bool muted); - void volumeChanged(); - void sourceChanged(); - void statusChanged(QMediaPlayer::MediaStatus status); - void playbackStateChanged(QMediaPlayer::State state); - void playbackRateChanged(qreal rate); - void errorChanged(QMediaPlayer::Error error); - void durationChanged(qint64 duration); - void positionChanged(qint64 position); - void seekableChanged(bool seekable); - void playing(); - void paused(); - void stopped(); - void canPlayChanged(); - void canPauseChanged(); - void canSkipForwardChanged(); - void canSkipBackwardChanged(); - void canGoNextChanged(); + void sleepTimerChanged(qint64 duration); + void remainingSleepTimeChanged(qint64 duration); + void logError(Error::Type type, const QString &url, const QString &id, const int errorId, const QString &errorString, const QString &title); public Q_SLOTS: void setEntry(Entry *entry); - void setMuted(bool muted); - void setVolume(qreal volume); - // void setSource(const QUrl &source); //source should only be set by audiomanager itself - void setPosition(qint64 position); - void setPlaybackRate(qreal rate); - void play(); - void pause(); - void playPause(); - void stop(); - void seek(qint64 position); - void skipBackward(); - void skipForward(); - void next(); + void setSleepTimer(qint64 duration); + void stopSleepTimer(); + private Q_SLOTS: void mediaStatusChanged(); - void playerStateChanged(); - void playerDurationChanged(qint64 duration); - void playerMutedChanged(); - void playerVolumeChanged(); - void savePlayPosition(); - void prepareAudio(); - void checkForPendingSeek(); private: diff --git a/src/qml/HeaderBar.qml b/src/qml/HeaderBar.qml index b840431c..87bec61f 100644 --- a/src/qml/HeaderBar.qml +++ b/src/qml/HeaderBar.qml @@ -110,7 +110,7 @@ Rectangle { Controls.ToolButton { id: chapterButton - visible: AudioManager.entry && chapterList.count !== 0 && (titlesAndButtons.width > essentialButtons.width + infoButton.implicitWidth + implicitWidth + parent.optionalButtonCollapseWidth) + visible: AudioManager.entry && chapterList.count !== 0 && (titlesAndButtons.width > essentialButtons.width + infoButton.implicitWidth + implicitWidth + 2 * infoButton.implicitWidth + parent.optionalButtonCollapseWidth) text: i18n("Chapters") icon.name: "view-media-playlist" icon.height: essentialButtons.iconSize @@ -121,7 +121,7 @@ Rectangle { } Controls.ToolButton { id: infoButton - visible: AudioManager.entry && (titlesAndButtons.width > essentialButtons.width + implicitWidth + parent.optionalButtonCollapseWidth) + visible: AudioManager.entry && (titlesAndButtons.width > essentialButtons.width + 2 * implicitWidth + parent.optionalButtonCollapseWidth) icon.name: "help-about-symbolic" icon.height: essentialButtons.iconSize icon.width: essentialButtons.iconSize @@ -129,6 +129,21 @@ Rectangle { Layout.preferredWidth: essentialButtons.buttonSize onClicked: entryDetailsOverlay.open(); } + Controls.ToolButton { + id: sleepButton + checkable: true + checked: AudioManager.remainingSleepTime > 0 + visible: titlesAndButtons.width > essentialButtons.width + implicitWidth + parent.optionalButtonCollapseWidth + icon.name: "clock" + icon.height: essentialButtons.iconSize + icon.width: essentialButtons.iconSize + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: essentialButtons.buttonSize + onClicked: { + toggle(); // only set the on/off state based on sleep timer state + sleepTimerDialog.open(); + } + } RowLayout { id: essentialButtons property int iconSize: Kirigami.Units.gridUnit diff --git a/src/qml/PlayerControls.qml b/src/qml/PlayerControls.qml index 2ba55128..6845b288 100644 --- a/src/qml/PlayerControls.qml +++ b/src/qml/PlayerControls.qml @@ -294,6 +294,24 @@ Kirigami.Page { 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() + } + } } Loader { diff --git a/src/qml/SleepTimerDialog.qml b/src/qml/SleepTimerDialog.qml new file mode 100644 index 00000000..33677931 --- /dev/null +++ b/src/qml/SleepTimerDialog.qml @@ -0,0 +1,96 @@ +/** + * SPDX-FileCopyrightText: 2022 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as Controls +import QtQuick.Layouts 1.14 + +import org.kde.kirigami 2.19 as Kirigami +import org.kde.kasts 1.0 + +Kirigami.Dialog { + id: sleepTimerDialog + title: i18n("Sleep Timer") + padding: Kirigami.Units.largeSpacing + closePolicy: Kirigami.Dialog.CloseOnEscape | Kirigami.Dialog.CloseOnPressOutside + standardButtons: Kirigami.Dialog.NoButton + + property bool timerActive: AudioManager.remainingSleepTime > 0 + + customFooterActions: [ + Kirigami.Action { + enabled: !timerActive + text: i18n("Start") + iconName: "dialog-ok" + onTriggered: { + sleepTimerDialog.close(); + var sleepTimeSeconds = sleepTimerValueBox.value * sleepTimerUnitsBox.model[sleepTimerUnitsBox.currentIndex]["secs"]; + if (sleepTimeSeconds > 0) { + SettingsManager.sleepTimerValue = sleepTimerValueBox.value; + SettingsManager.sleepTimerUnits = sleepTimerUnitsBox.currentValue; + SettingsManager.save(); + AudioManager.sleepTime = sleepTimeSeconds; + } + } + }, + Kirigami.Action { + enabled: timerActive + text: i18n("Stop") + iconName: "dialog-cancel" + onTriggered: { + sleepTimerDialog.close(); + AudioManager.sleepTime = undefined; // make use of RESET + } + } + ] + + ColumnLayout { + id: content + Text { + text: (timerActive) ? i18n("Status: Active") : i18n("Status: Inactive") + } + + Text { + opacity: (timerActive) ? 1 : 0.5 + Layout.bottomMargin: Kirigami.Units.largeSpacing + text: i18n("Remaining Time: %1", AudioManager.formattedRemainingSleepTime) + } + + RowLayout { + Controls.SpinBox { + id: sleepTimerValueBox + enabled: !timerActive + value: SettingsManager.sleepTimerValue + from: 1 + to: 24 * 60 * 60 + } + + Controls.ComboBox { + id: sleepTimerUnitsBox + enabled: !timerActive + textRole: "text" + valueRole: "value" + model: [{"text": i18n("Seconds"), "value": 0, "secs": 1, "max": 24 * 60 * 60}, + {"text": i18n("Minutes"), "value": 1, "secs": 60, "max": 24 * 60}, + {"text": i18n("Hours"), "value": 2, "secs": 60 * 60, "max": 24}] + Component.onCompleted: { + currentIndex = indexOfValue(SettingsManager.sleepTimerUnits); + sleepTimerValueBox.to = sleepTimerUnitsBox.model[currentIndex]["max"]; + if (sleepTimerValueBox.value > sleepTimerUnitsBox.model[currentIndex]["max"]) { + sleepTimerValueBox.value = sleepTimerUnitsBox.model[currentIndex]["max"]; + } + } + onActivated: { + SettingsManager.sleepTimerUnits = currentValue; + if (sleepTimerValueBox.value > sleepTimerUnitsBox.model[currentIndex]["max"]) { + sleepTimerValueBox.value = sleepTimerUnitsBox.model[currentIndex]["max"]; + } + sleepTimerValueBox.to = sleepTimerUnitsBox.model[currentIndex]["max"]; + } + } + } + } +} diff --git a/src/qml/main.qml b/src/qml/main.qml index 28e51af5..b6ee8640 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -355,6 +355,10 @@ Kirigami.ApplicationWindow { id: playbackRateDialog } + SleepTimerDialog { + id: sleepTimerDialog + } + Connections { target: Sync function onPasswordInputRequired() { diff --git a/src/resources.qrc b/src/resources.qrc index bbc26ea6..330d285b 100755 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -35,6 +35,7 @@ qml/Settings/StorageSettingsPage.qml qml/Settings/SynchronizationSettingsPage.qml qml/BottomToolbar.qml + qml/SleepTimerDialog.qml qtquickcontrols2.conf ../kasts.svg diff --git a/src/settingsmanager.kcfg b/src/settingsmanager.kcfg index 52e3afc3..9fdcece0 100644 --- a/src/settingsmanager.kcfg +++ b/src/settingsmanager.kcfg @@ -88,6 +88,28 @@ FeedListPage + + + 30 + + + + + + + 0 + + + + 1 + + + + 2 + + + Minutes +