diff --git a/images/web-radio.png b/images/web-radio.png
new file mode 100644
index 0000000..c68d7c1
Binary files /dev/null and b/images/web-radio.png differ
diff --git a/translations/webradioplugin_de.ts b/translations/webradioplugin_de.ts
new file mode 100644
index 0000000..5a9cdcb
--- /dev/null
+++ b/translations/webradioplugin_de.ts
@@ -0,0 +1,101 @@
+
+
+
+
+ WebRadioDialog
+
+
+ Radio
+ Radio
+
+
+
+ Play
+ Abspielen
+
+
+
+ Pause
+ Pause
+
+
+
+ Stop
+ Stop
+
+
+
+ Stopped
+ Gestoppt
+
+
+
+ Playing
+ Wird abgespielt
+
+
+
+ Paused
+ Pausiert
+
+
+
+
+ Unknown
+ Unbekannt
+
+
+
+ Unknown media
+ Unbekanntes Medium
+
+
+
+ No media
+ Kein Medium
+
+
+
+ Loading media...
+ Lade Medium...
+
+
+
+ Loaded media
+ Medium geladen
+
+
+
+ Stalled media
+ Medium angehalten
+
+
+
+ Buffering media
+ Puffere Medium
+
+
+
+ Buffered media
+ Medium gepuffert
+
+
+
+ End of media
+ Ende des Mediums
+
+
+
+ Invalid media
+ Ungültiges Medium
+
+
+
+ WebRadioPlugin
+
+
+ Play webradio
+ Webradio spielen
+
+
+
diff --git a/translations/webradioplugin_en.ts b/translations/webradioplugin_en.ts
new file mode 100644
index 0000000..7ba7a5b
--- /dev/null
+++ b/translations/webradioplugin_en.ts
@@ -0,0 +1,101 @@
+
+
+
+
+ WebRadioDialog
+
+
+ Radio
+
+
+
+
+ Play
+
+
+
+
+ Pause
+
+
+
+
+ Stop
+
+
+
+
+ Stopped
+
+
+
+
+ Playing
+
+
+
+
+ Paused
+
+
+
+
+
+ Unknown
+
+
+
+
+ Unknown media
+
+
+
+
+ No media
+
+
+
+
+ Loading media...
+
+
+
+
+ Loaded media
+
+
+
+
+ Stalled media
+
+
+
+
+ Buffering media
+
+
+
+
+ Buffered media
+
+
+
+
+ End of media
+
+
+
+
+ Invalid media
+
+
+
+
+ WebRadioPlugin
+
+
+ Play webradio
+
+
+
+
diff --git a/webradiodialog.cpp b/webradiodialog.cpp
new file mode 100644
index 0000000..236ee90
--- /dev/null
+++ b/webradiodialog.cpp
@@ -0,0 +1,128 @@
+#include "webradiodialog.h"
+#include "ui_webradiodialog.h"
+
+#include "mainwindow.h"
+#include "zeiterfassungsettings.h"
+
+WebRadioDialog::WebRadioDialog(MainWindow &mainWindow) :
+ ZeiterfassungDialog(&mainWindow),
+ ui(new Ui::WebRadioDialog),
+ m_mainWindow(mainWindow),
+ m_settings(m_mainWindow.settings())
+{
+ ui->setupUi(this);
+
+ m_player = new QMediaPlayer(this);
+ connect(m_player, &QMediaPlayer::stateChanged, this, &WebRadioDialog::stateChanged);
+ connect(m_player, &QMediaPlayer::stateChanged, this, &WebRadioDialog::updateWidgets);
+ connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &WebRadioDialog::mediaStatusChanged);
+ connect(m_player, &QMediaPlayer::mediaStatusChanged, this, &WebRadioDialog::updateWidgets);
+ connect(m_player, static_cast(&QMediaPlayer::error), this, &WebRadioDialog::error);
+
+ for(const auto &url : m_settings.urls())
+ ui->comboBox->addItem(url, url);
+
+ ui->comboBox->setCurrentIndex(ui->comboBox->findData(m_settings.lastUrl()));
+
+ connect(ui->comboBox, static_cast(&QComboBox::currentIndexChanged),
+ this, &WebRadioDialog::currentIndexChanged);
+
+ connect(ui->comboBox, static_cast(&QComboBox::currentIndexChanged),
+ this, &WebRadioDialog::updateWidgets);
+
+ connect(ui->pushButtonPlay, &QAbstractButton::pressed, this, &WebRadioDialog::play);
+ connect(ui->pushButtonPause, &QAbstractButton::pressed, m_player, &QMediaPlayer::pause);
+ connect(ui->pushButtonStop, &QAbstractButton::pressed, m_player, &QMediaPlayer::stop);
+
+ m_player->setVolume(m_settings.volume());
+ ui->horizontalSlider->setValue(m_player->volume());
+ connect(ui->horizontalSlider, &QAbstractSlider::valueChanged, this, &WebRadioDialog::volumeChanged);
+
+ stateChanged(m_player->state());
+ mediaStatusChanged(m_player->mediaStatus());
+ currentIndexChanged(ui->comboBox->currentIndex());
+ updateWidgets();
+}
+
+WebRadioDialog::~WebRadioDialog()
+{
+ // To avoid crash on app close
+ disconnect(m_player, &QMediaPlayer::stateChanged, this, &WebRadioDialog::stateChanged);
+ disconnect(m_player, &QMediaPlayer::stateChanged, this, &WebRadioDialog::updateWidgets);
+ disconnect(m_player, &QMediaPlayer::mediaStatusChanged, this, &WebRadioDialog::mediaStatusChanged);
+ disconnect(m_player, &QMediaPlayer::mediaStatusChanged, this, &WebRadioDialog::updateWidgets);
+ disconnect(m_player, static_cast(&QMediaPlayer::error), this, &WebRadioDialog::error);
+
+ delete ui;
+}
+
+void WebRadioDialog::stateChanged(QMediaPlayer::State newState)
+{
+ switch(newState)
+ {
+ case QMediaPlayer::StoppedState: ui->labelState->setText(tr("Stopped")); break;
+ case QMediaPlayer::PlayingState: ui->labelState->setText(tr("Playing")); break;
+ case QMediaPlayer::PausedState: ui->labelState->setText(tr("Paused")); break;
+ default:
+ qWarning() << "unknown state" << newState;
+ ui->labelState->setText(tr("Unknown"));
+ }
+}
+
+void WebRadioDialog::mediaStatusChanged(QMediaPlayer::MediaStatus status)
+{
+ switch(status)
+ {
+ case QMediaPlayer::UnknownMediaStatus: ui->labelMediaStatus->setText(tr("Unknown media")); break;
+ case QMediaPlayer::NoMedia: ui->labelMediaStatus->setText(tr("No media")); break;
+ case QMediaPlayer::LoadingMedia: ui->labelMediaStatus->setText(tr("Loading media...")); break;
+ case QMediaPlayer::LoadedMedia: ui->labelMediaStatus->setText(tr("Loaded media")); break;
+ case QMediaPlayer::StalledMedia: ui->labelMediaStatus->setText(tr("Stalled media")); break;
+ case QMediaPlayer::BufferingMedia: ui->labelMediaStatus->setText(tr("Buffering media")); break;
+ case QMediaPlayer::BufferedMedia: ui->labelMediaStatus->setText(tr("Buffered media")); break;
+ case QMediaPlayer::EndOfMedia: ui->labelMediaStatus->setText(tr("End of media")); break;
+ case QMediaPlayer::InvalidMedia: ui->labelMediaStatus->setText(tr("Invalid media")); break;
+ default:
+ qWarning() << "unknown mediaStatus" << status;
+ ui->labelMediaStatus->setText(tr("Unknown"));
+ }
+}
+
+void WebRadioDialog::error(QMediaPlayer::Error error)
+{
+ qWarning() << error;
+}
+
+void WebRadioDialog::currentIndexChanged(int index)
+{
+ if(index == -1)
+ m_player->setMedia(QMediaContent());
+ else
+ m_player->setMedia(QMediaContent(QUrl(ui->comboBox->currentData().toString())));
+}
+
+void WebRadioDialog::play()
+{
+ if(ui->comboBox->currentIndex() == -1)
+ return;
+
+ m_settings.setLastUrl(ui->comboBox->currentData().toString());
+
+ m_player->play();
+}
+
+void WebRadioDialog::updateWidgets()
+{
+ ui->comboBox->setEnabled(m_player->state() != QMediaPlayer::PlayingState);
+ ui->pushButtonPlay->setEnabled(ui->comboBox->currentIndex() > -1 &&
+ (m_player->state() == QMediaPlayer::StoppedState || m_player->state() == QMediaPlayer::PausedState) &&
+ (m_player->mediaStatus() == QMediaPlayer::LoadedMedia || m_player->mediaStatus() == QMediaPlayer::BufferedMedia));
+ ui->pushButtonPause->setEnabled(m_player->state() == QMediaPlayer::PlayingState || m_player->state() == QMediaPlayer::PausedState);
+ ui->pushButtonStop->setEnabled(m_player->state() == QMediaPlayer::PlayingState || m_player->state() == QMediaPlayer::PausedState);
+}
+
+void WebRadioDialog::volumeChanged(int volume)
+{
+ m_settings.setVolume(volume);
+ m_player->setVolume(volume);
+}
diff --git a/webradiodialog.h b/webradiodialog.h
new file mode 100644
index 0000000..b1c6c73
--- /dev/null
+++ b/webradiodialog.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+
+#include "zeiterfassungdialog.h"
+
+#include "webradiosettings.h"
+
+class MainWindow;
+
+namespace Ui { class WebRadioDialog; }
+
+class WebRadioDialog : public ZeiterfassungDialog
+{
+ Q_OBJECT
+
+public:
+ explicit WebRadioDialog(MainWindow &mainWindow);
+ ~WebRadioDialog();
+
+private Q_SLOTS:
+ void stateChanged(QMediaPlayer::State newState);
+ void mediaStatusChanged(QMediaPlayer::MediaStatus status);
+ void error(QMediaPlayer::Error error);
+ void currentIndexChanged(int index);
+ void volumeChanged(int volume);
+ void play();
+
+private:
+ void updateWidgets();
+
+ Ui::WebRadioDialog *ui;
+
+ MainWindow &m_mainWindow;
+ WebRadioSettings m_settings;
+
+ QMediaPlayer *m_player;
+};
diff --git a/webradiodialog.ui b/webradiodialog.ui
new file mode 100644
index 0000000..5758e10
--- /dev/null
+++ b/webradiodialog.ui
@@ -0,0 +1,135 @@
+
+
+ WebRadioDialog
+
+
+
+ 0
+ 0
+ 494
+ 155
+
+
+
+ Radio
+
+
+ -
+
+
+ -
+
+
-
+
+
+
+ 0
+ 50
+
+
+
+ Play
+
+
+
+ -
+
+
+
+ 0
+ 50
+
+
+
+ Pause
+
+
+
+ -
+
+
+
+ 0
+ 50
+
+
+
+ Stop
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
+
+ -
+
+
-
+
+
+ state
+
+
+
+ -
+
+
+ mediaStatus
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Close
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ WebRadioDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ WebRadioDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/webradioplugin.cpp b/webradioplugin.cpp
new file mode 100644
index 0000000..d75bfcb
--- /dev/null
+++ b/webradioplugin.cpp
@@ -0,0 +1,45 @@
+#include "webradioplugin.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "mainwindow.h"
+
+#include "webradiodialog.h"
+#include "webradiosettingswidget.h"
+
+WebRadioPlugin::WebRadioPlugin(QObject *parent) :
+ ZeiterfassungPlugin(parent)
+{
+ qDebug() << "called";
+
+ static auto dir = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(QStringLiteral("translations"));
+
+ if(m_translator.load(QLocale(), QStringLiteral("webradioplugin"), QStringLiteral("_"), dir))
+ {
+ if(!QCoreApplication::installTranslator(&m_translator))
+ {
+ qWarning() << "could not install translation webradioplugin";
+ }
+ }
+ else
+ {
+ qWarning() << "could not load translation webradioplugin";
+ }
+}
+
+void WebRadioPlugin::attachTo(MainWindow &mainWindow)
+{
+ auto dialog = new WebRadioDialog(mainWindow);
+ mainWindow.menuTools()->addAction(QIcon(QStringLiteral(":/zeiterfassung/plugins/webradioplugin/images/web-radio.png")),
+ tr("Play webradio"), dialog, &QWidget::show);
+}
+
+SettingsWidget *WebRadioPlugin::settingsWidget(ZeiterfassungSettings &settings, QWidget *parent) const
+{
+ return new WebRadioSettingsWidget(settings, parent);
+}
diff --git a/webradioplugin.h b/webradioplugin.h
new file mode 100644
index 0000000..6644b3f
--- /dev/null
+++ b/webradioplugin.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include
+#include
+
+#include "zeiterfassungplugin.h"
+
+class Q_DECL_EXPORT WebRadioPlugin : public ZeiterfassungPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "dbsoftware.zeiterfassung.plugin/1.0" FILE "webradioplugin.json")
+ Q_INTERFACES(ZeiterfassungPlugin)
+
+public:
+ explicit WebRadioPlugin(QObject *parent = Q_NULLPTR);
+
+ // ZeiterfassungPlugin interface
+ void attachTo(MainWindow &mainWindow) Q_DECL_OVERRIDE;
+
+ SettingsWidget *settingsWidget(ZeiterfassungSettings &settings, QWidget *parent = Q_NULLPTR) const Q_DECL_OVERRIDE;
+
+private:
+ QTranslator m_translator;
+};
diff --git a/webradioplugin.json b/webradioplugin.json
new file mode 100644
index 0000000..e69de29
diff --git a/webradioplugin.pro b/webradioplugin.pro
new file mode 100644
index 0000000..2999d4b
--- /dev/null
+++ b/webradioplugin.pro
@@ -0,0 +1,26 @@
+QT += core network gui widgets multimedia
+
+DBLIBS += dbgui zeiterfassungcore zeiterfassunggui
+
+TARGET = webradioplugin
+
+HEADERS += webradiodialog.h \
+ webradioplugin.h \
+ webradiosettings.h \
+ webradiosettingswidget.h
+
+SOURCES += webradiodialog.cpp \
+ webradioplugin.cpp \
+ webradiosettings.cpp \
+ webradiosettingswidget.cpp
+
+FORMS += webradiodialog.ui
+
+RESOURCES += webradioplugin_resources.qrc
+
+TRANSLATIONS += translations/webradioplugin_en.ts \
+ translations/webradioplugin_de.ts
+
+OTHER_FILES += webradioplugin.json
+
+include(../plugin.pri)
diff --git a/webradioplugin_resources.qrc b/webradioplugin_resources.qrc
new file mode 100644
index 0000000..f7b12b5
--- /dev/null
+++ b/webradioplugin_resources.qrc
@@ -0,0 +1,5 @@
+
+
+ images/web-radio.png
+
+
diff --git a/webradiosettings.cpp b/webradiosettings.cpp
new file mode 100644
index 0000000..615c44a
--- /dev/null
+++ b/webradiosettings.cpp
@@ -0,0 +1,114 @@
+#include "webradiosettings.h"
+
+#include "zeiterfassungsettings.h"
+
+const QString WebRadioSettings::m_urls("WebRadioPlugin/urls");
+const QString WebRadioSettings::m_lastUrl("WebRadioPlugin/lastUrl");
+const QString WebRadioSettings::m_volume("WebRadioPlugin/volume");
+const QStringList WebRadioSettings::m_defaultUrls {
+ QStringLiteral("http://stream.drumandbass.fm:9002"),
+ QStringLiteral("http://stream.trap.fm:6002"),
+ QStringLiteral("http://stream.dubbase.fm:7002"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/hb.mp3"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/tb.mp3"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/tt.mp3"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/ht.mp3"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/trb.mp3"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/ct.mp3"),
+ QStringLiteral("http://lw1.mp3.tb-group.fm/clt.mp3"),
+ QStringLiteral("https://live.helsinki.at:8088/live160.ogg")
+};
+const int WebRadioSettings::m_defaultVolume(100);
+
+WebRadioSettings::WebRadioSettings(ZeiterfassungSettings &settings, QObject *parent) :
+ QObject(parent),
+ m_settings(settings)
+{
+}
+
+QStringList WebRadioSettings::urls() const
+{
+ return m_settings.value(m_urls, m_defaultUrls).toStringList();
+}
+
+bool WebRadioSettings::setUrls(const QStringList &urls)
+{
+ if(this->urls() == urls)
+ return true;
+
+ if(urls == m_defaultUrls)
+ m_settings.remove(m_urls);
+ else
+ m_settings.setValue(m_urls, urls);
+
+ m_settings.sync();
+
+ const auto success = m_settings.status() == QSettings::NoError;
+ if(success)
+ Q_EMIT urlsChanged(urls);
+ else
+ {
+ Q_EMIT m_settings.saveErrorOccured();
+ Q_EMIT saveErrorOccured();
+ }
+
+ return success;
+}
+
+QString WebRadioSettings::lastUrl() const
+{
+ return m_settings.value(m_lastUrl).toString();
+}
+
+bool WebRadioSettings::setLastUrl(const QString &lastUrl)
+{
+ if(this->lastUrl() == lastUrl)
+ return true;
+
+ if(lastUrl.isNull())
+ m_settings.remove(m_lastUrl);
+ else
+ m_settings.setValue(m_lastUrl, lastUrl);
+
+ m_settings.sync();
+
+ const auto success = m_settings.status() == QSettings::NoError;
+ if(success)
+ Q_EMIT lastUrlChanged(lastUrl);
+ else
+ {
+ Q_EMIT m_settings.saveErrorOccured();
+ Q_EMIT saveErrorOccured();
+ }
+
+ return success;
+}
+
+int WebRadioSettings::volume() const
+{
+ return m_settings.value(m_volume, m_defaultVolume).toInt();
+}
+
+bool WebRadioSettings::setVolume(int volume)
+{
+ if(this->volume() == volume)
+ return true;
+
+ if(volume == m_defaultVolume)
+ m_settings.remove(m_volume);
+ else
+ m_settings.setValue(m_volume, volume);
+
+ m_settings.sync();
+
+ const auto success = m_settings.status() == QSettings::NoError;
+ if(success)
+ Q_EMIT volumeChanged(volume);
+ else
+ {
+ Q_EMIT m_settings.saveErrorOccured();
+ Q_EMIT saveErrorOccured();
+ }
+
+ return success;
+}
diff --git a/webradiosettings.h b/webradiosettings.h
new file mode 100644
index 0000000..0e8ceb6
--- /dev/null
+++ b/webradiosettings.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include
+#include
+
+class ZeiterfassungSettings;
+
+class WebRadioSettings : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QStringList urls READ urls WRITE setUrls NOTIFY urlsChanged)
+ Q_PROPERTY(QString lastUrl READ lastUrl WRITE setLastUrl NOTIFY lastUrlChanged)
+ Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged)
+
+public:
+ WebRadioSettings(ZeiterfassungSettings &settings, QObject *parent = Q_NULLPTR);
+
+ QStringList urls() const;
+ bool setUrls(const QStringList &urls);
+
+ QString lastUrl() const;
+ bool setLastUrl(const QString &lastUrl);
+
+ int volume() const;
+ bool setVolume(int volume);
+
+Q_SIGNALS:
+ void saveErrorOccured();
+
+ void urlsChanged(const QStringList &urls);
+ void lastUrlChanged(const QString &lastUrl);
+ void volumeChanged(int volume);
+
+private:
+ ZeiterfassungSettings &m_settings;
+
+ static const QString m_urls;
+ static const QString m_lastUrl;
+ static const QString m_volume;
+ static const QStringList m_defaultUrls;
+ static const int m_defaultVolume;
+};
diff --git a/webradiosettingswidget.cpp b/webradiosettingswidget.cpp
new file mode 100644
index 0000000..04f8b56
--- /dev/null
+++ b/webradiosettingswidget.cpp
@@ -0,0 +1,37 @@
+#include "webradiosettingswidget.h"
+
+#include
+
+#include "stringlistwidget.h"
+
+WebRadioSettingsWidget::WebRadioSettingsWidget(ZeiterfassungSettings &settings, QWidget *parent) :
+ SettingsWidget(parent),
+ m_settings(settings)
+{
+ auto layout = new QFormLayout(this);
+ layout->setMargin(0);
+
+ m_urlsWidget = new StringListWidget(m_settings.urls(), this);
+ layout->addRow(tr("URLs:"), m_urlsWidget);
+
+ setLayout(layout);
+}
+
+bool WebRadioSettingsWidget::isValid(QString &message) const
+{
+ for(const auto &url : m_urlsWidget->value())
+ {
+ if(!QUrl::fromUserInput(url).isValid())
+ {
+ message = tr("A web radio url is invalid!");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool WebRadioSettingsWidget::apply()
+{
+ return m_settings.setUrls(m_urlsWidget->value());
+}
diff --git a/webradiosettingswidget.h b/webradiosettingswidget.h
new file mode 100644
index 0000000..04f0181
--- /dev/null
+++ b/webradiosettingswidget.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include
+
+#include "settingswidget.h"
+
+#include "webradiosettings.h"
+
+class StringListWidget;
+
+class ZeiterfassungSettings;
+
+class WebRadioSettingsWidget : public SettingsWidget
+{
+ Q_OBJECT
+
+public:
+ explicit WebRadioSettingsWidget(ZeiterfassungSettings &settings, QWidget *parent = Q_NULLPTR);
+
+ bool isValid(QString &message) const Q_DECL_OVERRIDE;
+
+public Q_SLOTS:
+ virtual bool apply() Q_DECL_OVERRIDE;
+
+private:
+ WebRadioSettings m_settings;
+
+ StringListWidget *m_urlsWidget;
+};