Add sleep timer

FEATURE: 443400
This commit is contained in:
Bart De Vries 2022-06-30 08:21:09 +00:00
parent ac58ad0a1a
commit 5f94b4a357
9 changed files with 246 additions and 57 deletions

View File

@ -222,6 +222,7 @@ if(ANDROID)
emblem-music-symbolic
gpodder
kaccounts-nextcloud
clock
)
else()
target_link_libraries(kasts PRIVATE Qt::Widgets Qt::DBus)

View File

@ -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);
}

View File

@ -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:

View File

@ -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

View File

@ -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 {

View File

@ -0,0 +1,96 @@
/**
* SPDX-FileCopyrightText: 2022 Bart De Vries <bart@mogwai.be>
*
* 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"];
}
}
}
}
}

View File

@ -355,6 +355,10 @@ Kirigami.ApplicationWindow {
id: playbackRateDialog
}
SleepTimerDialog {
id: sleepTimerDialog
}
Connections {
target: Sync
function onPasswordInputRequired() {

View File

@ -35,6 +35,7 @@
<file alias="StorageSettingsPage.qml">qml/Settings/StorageSettingsPage.qml</file>
<file alias="SynchronizationSettingsPage.qml">qml/Settings/SynchronizationSettingsPage.qml</file>
<file alias="BottomToolbar.qml">qml/BottomToolbar.qml</file>
<file alias="SleepTimerDialog.qml">qml/SleepTimerDialog.qml</file>
<file>qtquickcontrols2.conf</file>
<file alias="logo.svg">../kasts.svg</file>
</qresource>

View File

@ -88,6 +88,28 @@
<label>The top-level page that was open at shutdown</label>
<default>FeedListPage</default>
</entry>
<entry name="sleepTimerValue" type="Int">
<label>The number of seconds/minutes/hours to set the sleep timer to</label>
<default>30</default>
</entry>
<entry name="sleepTimerUnits" type="Enum">
<label>The units for the sleepTimerValue</label>
<choices>
<choice name="Seconds">
<label>Seconds</label>
<value>0</value>
</choice>
<choice name="Minutes">
<label>Minutes</label>
<value>1</value>
</choice>
<choice name="Hours">
<label>Hours</label>
<value>2</value>
</choice>
</choices>
<default>Minutes</default>
</entry>
</group>
<group name="Synchronization">
<entry name="syncEnabled" type="Bool">