Add podcasts support WIP

This commit is contained in:
Jonas Kvinge 2021-07-16 05:06:41 +02:00
parent c52fc90306
commit 7faa0df01e
64 changed files with 8466 additions and 0 deletions

View File

@ -349,6 +349,12 @@ if(WIN32)
add_definitions(-DSTATIC_GETOPT -D_UNICODE)
endif()
if(BUILD_WITH_QT6)
pkg_check_modules(LIBMYGPO libmygpo-qt6)
else()
pkg_check_modules(LIBMYGPO libmygpo-qt5)
endif()
if(WIN32 AND NOT MSVC)
# RC compiler
string(REPLACE "gcc" "windres" CMAKE_RC_COMPILER_INIT ${CMAKE_C_COMPILER})
@ -462,6 +468,8 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization"
DEPENDS "gstreamer" HAVE_GSTREAMER
)
optional_component(PODCASTS ON "Podcasts support" DEPENDS "libmygpo" LIBMYGPO_FOUND)
if(APPLE OR WIN32)
set(USE_BUNDLE_DEFAULT ON)
else()

View File

@ -939,11 +939,75 @@ optional_source(HAVE_MOODBAR
settings/moodbarsettingspage.ui
)
# EBU R 128
optional_source(HAVE_EBUR128
SOURCES engine/ebur128analysis.cpp
)
optional_source(HAVE_PODCASTS
SOURCES
podcasts/gpoddersync.cpp
podcasts/gpoddertoptagsmodel.cpp
podcasts/gpoddertoptagspage.cpp
podcasts/itunessearchpage.cpp
podcasts/podcastbackend.cpp
podcasts/podcastservice.cpp
podcasts/podcast.cpp
podcasts/podcastdownloader.cpp
podcasts/podcastupdater.cpp
podcasts/podcastdeleter.cpp
podcasts/podcastdiscoverymodel.cpp
podcasts/podcastepisode.cpp
podcasts/podcastinfodialog.cpp
podcasts/podcastinfowidget.cpp
podcasts/podcastparser.cpp
podcasts/podcastservicemodel.cpp
podcasts/podcasturlloader.cpp
podcasts/gpoddersearchpage.cpp
podcasts/addpodcastbyurl.cpp
podcasts/addpodcastdialog.cpp
podcasts/addpodcastpage.cpp
podcasts/episodeinfowidget.cpp
podcasts/fixedopmlpage.cpp
settings/podcastsettingspage.cpp
HEADERS
podcasts/addpodcastbyurl.h
podcasts/addpodcastdialog.h
podcasts/addpodcastpage.h
podcasts/episodeinfowidget.h
podcasts/fixedopmlpage.h
podcasts/gpoddersync.h
podcasts/gpoddertoptagsmodel.h
podcasts/gpoddertoptagspage.h
podcasts/itunessearchpage.h
podcasts/opmlcontainer.h
podcasts/podcastbackend.h
podcasts/podcastdeleter.h
podcasts/podcastdiscoverymodel.h
podcasts/podcastdownloader.h
podcasts/podcastepisode.h
podcasts/podcast.h
podcasts/podcastinfodialog.h
podcasts/podcastinfowidget.h
podcasts/podcastparser.h
podcasts/podcastservice.h
podcasts/podcastservicemodel.h
podcasts/podcastupdater.h
podcasts/podcasturlloader.h
podcasts/gpoddersearchpage.h
settings/podcastsettingspage.h
UI
podcasts/addpodcastbyurl.ui
podcasts/addpodcastdialog.ui
podcasts/episodeinfowidget.ui
podcasts/itunessearchpage.ui
podcasts/podcastinfodialog.ui
podcasts/podcastinfowidget.ui
podcasts/gpoddersearchpage.ui
settings/podcastsettingspage.ui
)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
@ -1059,6 +1123,10 @@ if(USE_TAGPARSER AND TAGPARSER_FOUND)
link_directories(${TAGPARSER_LIBRARY_DIRS})
endif()
if(HAVE_PODCASTS)
link_directories(${LIBMYGPO_LIBRARY_DIRS})
endif()
if(HAVE_QTSPARKLE)
link_directories(${QTSPARKLE_LIBRARY_DIRS})
endif()
@ -1214,6 +1282,11 @@ if(HAVE_LIBMTP)
target_link_libraries(strawberry_lib PRIVATE ${LIBMTP_LIBRARIES})
endif()
if(HAVE_PODCASTS)
target_include_directories(strawberry_lib SYSTEM PRIVATE ${LIBMYGPO_INCLUDE_DIRS})
target_link_libraries(strawberry_lib PRIVATE ${LIBMYGPO_LIBRARIES})
endif(HAVE_PODCASTS)
if(APPLE)
target_link_libraries(strawberry_lib PRIVATE
"-framework AppKit"

View File

@ -33,6 +33,7 @@
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_MOODBAR
#cmakedefine HAVE_PODCASTS
#cmakedefine HAVE_KEYSYMDEF_H
#cmakedefine HAVE_XF86KEYSYM_H

View File

@ -102,7 +102,16 @@
#include "radios/radioservices.h"
#include "radios/radiobackend.h"
#ifdef HAVE_PODCASTS
# include "podcasts/podcastbackend.h"
# include "podcasts/gpoddersync.h"
# include "podcasts/podcastdownloader.h"
# include "podcasts/podcastupdater.h"
# include "podcasts/podcastdeleter.h"
#endif
using std::make_shared;
using namespace std::chrono_literals;
class ApplicationImpl {
@ -200,6 +209,21 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
moodbar_controller_([app]() { return new MoodbarController(app); }),
#endif
#ifdef HAVE_PODCASTS
podcast_backend_([app]() {
PodcastBackend* backend = new PodcastBackend(app, app);
app->MoveToThread(backend, database_->thread());
return backend;
}),
gpodder_sync_([app]() { return new GPodderSync(app, app); }),
podcast_downloader_([app]() { return new PodcastDownloader(app, app); }),
podcast_updater_([app]() { return new PodcastUpdater(app, app); }),
podcast_deleter_([app]() {
PodcastDeleter* deleter = new PodcastDeleter(app, app);
app->MoveToNewThread(deleter);
return deleter;
}),
#endif
lastfm_import_([app]() { return new LastFMImport(app->network()); })
{}
@ -226,6 +250,13 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
#endif
#ifdef HAVE_PODCASTS
Lazy<PodcastBackend> podcast_backend_;
Lazy<GPodderSync> gpodder_sync_;
Lazy<PodcastDownloader> podcast_downloader_;
Lazy<PodcastUpdater> podcast_updater_;
Lazy<PodcastDeleter> podcast_deleter_;
#endif
Lazy<LastFMImport> lastfm_import_;
@ -359,3 +390,10 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
#endif
#ifdef HAVE_PODCASTS
PodcastBackend *Application::podcast_backend() const { return p_->podcast_backend_.get(); }
GPodderSync *Application::gpodder_sync() const { return p_->gpodder_sync_.get(); }
PodcastDownloader *Application::podcast_downloader() const { return p_->podcast_downloader_.get(); }
PodcastUpdater *Application::podcast_updater() const { return p_->podcast_updater_.get(); }
PodcastDeleter *Application::podcast_deleter() const { return p_->podcast_deleter_.get(); }
#endif

View File

@ -64,6 +64,14 @@ class RadioServices;
class MoodbarController;
class MoodbarLoader;
#endif
#ifdef HAVE_PODCASTS
class PodcastBackend;
class GPodderSync;
class PodcastDownloader;
class PodcastUpdater;
class PodcastDeleter;
#endif
class Application : public QObject {
Q_OBJECT
@ -104,6 +112,13 @@ class Application : public QObject {
SharedPtr<MoodbarController> moodbar_controller() const;
SharedPtr<MoodbarLoader> moodbar_loader() const;
#endif
#ifdef HAVE_PODCASTS
PodcastBackend *podcast_backend() const;
GPodderSync *gpodder_sync() const;
PodcastDownloader *podcast_downloader() const;
PodcastUpdater *podcast_updater() const;
PodcastDeleter *podcast_deleter() const;
#endif
SharedPtr<LastFMImport> lastfm_import() const;

View File

@ -0,0 +1,116 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QString>
#include <QUrl>
#include <QClipboard>
#include <QMessageBox>
#include "core/iconloader.h"
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "addpodcastbyurl.h"
#include "ui_addpodcastbyurl.h"
AddPodcastByUrl::AddPodcastByUrl(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
ui_(new Ui_AddPodcastByUrl),
loader_(new PodcastUrlLoader(this)) {
ui_->setupUi(this);
QObject::connect(ui_->go, &QPushButton::clicked, this, &AddPodcastByUrl::GoClicked);
setWindowIcon(IconLoader::Load("podcast"));
}
AddPodcastByUrl::~AddPodcastByUrl() { delete ui_; }
void AddPodcastByUrl::SetUrlAndGo(const QUrl &url) {
ui_->url->setText(url.toString());
GoClicked();
}
void AddPodcastByUrl::SetOpml(const OpmlContainer &opml) {
ui_->url->setText(opml.url.toString());
model()->clear();
model()->CreateOpmlContainerItems(opml, model()->invisibleRootItem());
}
void AddPodcastByUrl::GoClicked() {
emit Busy(true);
model()->clear();
PodcastUrlLoaderReply* reply = loader_->Load(ui_->url->text());
ui_->url->setText(reply->url().toString());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { RequestFinished(reply); });
}
void AddPodcastByUrl::RequestFinished(PodcastUrlLoaderReply *reply) {
reply->deleteLater();
emit Busy(false);
if (!reply->is_success()) {
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
return;
}
switch (reply->result_type()) {
case PodcastUrlLoaderReply::Type_Podcast:
for (const Podcast& podcast : reply->podcast_results()) {
model()->appendRow(model()->CreatePodcastItem(podcast));
}
break;
case PodcastUrlLoaderReply::Type_Opml:
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
break;
}
}
void AddPodcastByUrl::Show() {
ui_->url->setFocus();
const QClipboard *clipboard = QApplication::clipboard();
QStringList contents;
contents << clipboard->text(QClipboard::Selection) << clipboard->text(QClipboard::Clipboard);
for (const QString &content : contents) {
if (content.contains("://")) {
ui_->url->setText(content);
return;
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ADDPODCASTBYURL_H
#define ADDPODCASTBYURL_H
#include <QObject>
#include <QUrl>
#include "addpodcastpage.h"
#include "opmlcontainer.h"
class Application;
class AddPodcastPage;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class Ui_AddPodcastByUrl;
class AddPodcastByUrl : public AddPodcastPage {
Q_OBJECT
public:
explicit AddPodcastByUrl(Application *app, QWidget *parent = nullptr);
~AddPodcastByUrl();
void Show();
void SetOpml(const OpmlContainer &opml);
void SetUrlAndGo(const QUrl &url);
private slots:
void GoClicked();
void RequestFinished(PodcastUrlLoaderReply *reply);
private:
Ui_AddPodcastByUrl *ui_;
PodcastUrlLoader *loader_;
};
#endif // ADDPODCASTBYURL_H

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastByUrl</class>
<widget class="QWidget" name="AddPodcastByUrl">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>431</width>
<height>51</height>
</rect>
</property>
<property name="windowTitle">
<string>Enter a URL</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>If you know the URL of a podcast, enter it below and press Go.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="url"/>
</item>
<item>
<widget class="QPushButton" name="go">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>url</sender>
<signal>returnPressed()</signal>
<receiver>go</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>109</x>
<y>24</y>
</hint>
<hint type="destinationlabel">
<x>429</x>
<y>49</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,270 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QDialog>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QDir>
#include <QFileDialog>
#include <QTimer>
#include <QPushButton>
#include <QListWidget>
#include <QItemSelectionModel>
#include "core/application.h"
#include "core/iconloader.h"
#include "widgets/widgetfadehelper.h"
#include "fixedopmlpage.h"
#include "gpoddersearchpage.h"
#include "gpoddertoptagspage.h"
#include "itunessearchpage.h"
#include "podcastbackend.h"
#include "podcastdiscoverymodel.h"
#include "addpodcastbyurl.h"
#include "podcastinfowidget.h"
#include "addpodcastdialog.h"
#include "ui_addpodcastdialog.h"
const char *AddPodcastDialog::kBbcOpmlUrl = "http://www.bbc.co.uk/podcasts.opml";
const char *AddPodcastDialog::kCbcOpmlUrl = "http://cbc.ca/podcasts.opml";
AddPodcastDialog::AddPodcastDialog(Application *app, QWidget *parent)
: QDialog(parent),
app_(app),
ui_(new Ui_AddPodcastDialog),
last_opml_path_(QDir::homePath()) {
ui_->setupUi(this);
ui_->details->SetApplication(app);
ui_->results->SetExpandOnReset(false);
ui_->results->SetAddOnDoubleClick(false);
ui_->results_stack->setCurrentWidget(ui_->results_page);
fader_ = new WidgetFadeHelper(ui_->details_scroll_area);
QObject::connect(ui_->provider_list, &QListWidget::currentRowChanged, this, &AddPodcastDialog::ChangePage);
QObject::connect(ui_->details, &PodcastInfoWidget::LoadingFinished, fader_, &WidgetFadeHelper::StartFade);
QObject::connect(ui_->results, &AutoExpandingTreeView::doubleClicked, this, &AddPodcastDialog::PodcastDoubleClicked);
// Create Add and Remove Podcast buttons
add_button_ = new QPushButton(IconLoader::Load("list-add"), tr("Add Podcast"), this);
add_button_->setEnabled(false);
connect(add_button_, &QPushButton::clicked, this, &AddPodcastDialog::AddPodcast);
ui_->button_box->addButton(add_button_, QDialogButtonBox::ActionRole);
remove_button_ = new QPushButton(IconLoader::Load("list-remove"), tr("Unsubscribe"), this);
remove_button_->setEnabled(false);
connect(remove_button_, &QPushButton::clicked, this, &AddPodcastDialog::RemovePodcast);
ui_->button_box->addButton(remove_button_, QDialogButtonBox::ActionRole);
QPushButton *settings_button = new QPushButton(IconLoader::Load("configure"), tr("Configure podcasts..."), this);
connect(settings_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenSettingsPage);
ui_->button_box->addButton(settings_button, QDialogButtonBox::ResetRole);
// Create an Open OPML file button
QPushButton *open_opml_button = new QPushButton(IconLoader::Load("document-open"), tr("Open OPML file..."), this);
QObject::connect(open_opml_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenOPMLFile);
ui_->button_box->addButton(open_opml_button, QDialogButtonBox::ResetRole);
// Add providers
by_url_page_ = new AddPodcastByUrl(app, this);
AddPage(by_url_page_);
AddPage(new FixedOpmlPage(QUrl(kBbcOpmlUrl), tr("BBC Podcasts"), IconLoader::Load("bbc"), app, this));
AddPage(new FixedOpmlPage(QUrl(kCbcOpmlUrl), tr("CBC Podcasts"), IconLoader::Load("cbc"), app, this));
AddPage(new GPodderTopTagsPage(app, this));
AddPage(new GPodderSearchPage(app, this));
AddPage(new ITunesSearchPage(app, this));
ui_->provider_list->setCurrentRow(0);
}
AddPodcastDialog::~AddPodcastDialog() { delete ui_; }
void AddPodcastDialog::ShowWithUrl(const QUrl& url) {
by_url_page_->SetUrlAndGo(url);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::ShowWithOpml(const OpmlContainer& opml) {
by_url_page_->SetOpml(opml);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::AddPage(AddPodcastPage *page) {
pages_.append(page);
page_is_busy_.append(false);
ui_->stack->addWidget(page);
new QListWidgetItem(page->windowIcon(), page->windowTitle(), ui_->provider_list);
QObject::connect(page, &AddPodcastPage::Busy, this, &AddPodcastDialog::PageBusyChanged);
}
void AddPodcastDialog::ChangePage(const int index) {
AddPodcastPage *page = pages_[index];
ui_->stack->setCurrentIndex(index);
ui_->stack->setVisible(page->has_visible_widget());
ui_->results->setModel(page->model());
ui_->results_stack->setCurrentWidget(page_is_busy_[index] ? ui_->busy_page : ui_->results_page);
QObject::connect(ui_->results->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &AddPodcastDialog::ChangePodcast);
ChangePodcast(QModelIndex());
CurrentPageBusyChanged(page_is_busy_[index]);
page->Show();
}
void AddPodcastDialog::ChangePodcast(const QModelIndex &current) {
QVariant podcast_variant = current.data(PodcastDiscoveryModel::Role_Podcast);
// If the selected item is invalid or not a podcast, hide the details pane.
if (podcast_variant.isNull()) {
ui_->details_scroll_area->hide();
add_button_->setEnabled(false);
remove_button_->setEnabled(false);
return;
}
current_podcast_ = podcast_variant.value<Podcast>();
// Start the blur+fade if there's already a podcast in the details pane.
if (ui_->details_scroll_area->isVisible()) {
fader_->StartBlur();
}
else {
ui_->details_scroll_area->show();
}
// Update the details pane
ui_->details->SetPodcast(current_podcast_);
// Is the user already subscribed to this podcast?
Podcast subscribed_podcast = app_->podcast_backend()->GetSubscriptionByUrl(current_podcast_.url());
const bool is_subscribed = subscribed_podcast.url().isValid();
if (is_subscribed) {
// Use the one from the database which will contain the ID.
current_podcast_ = subscribed_podcast;
}
add_button_->setEnabled(!is_subscribed);
remove_button_->setEnabled(is_subscribed);
}
void AddPodcastDialog::PageBusyChanged(const bool busy) {
const int index = pages_.indexOf(qobject_cast<AddPodcastPage*>(sender()));
if (index == -1) return;
page_is_busy_[index] = busy;
if (index == ui_->provider_list->currentRow()) {
CurrentPageBusyChanged(busy);
}
}
void AddPodcastDialog::CurrentPageBusyChanged(const bool busy) {
ui_->results_stack->setCurrentWidget(busy ? ui_->busy_page : ui_->results_page);
ui_->stack->setDisabled(busy);
QTimer::singleShot(0, this, &AddPodcastDialog::SelectFirstPodcast);
}
void AddPodcastDialog::SelectFirstPodcast() {
// Select the first item if there was one.
const PodcastDiscoveryModel *model = pages_[ui_->provider_list->currentRow()]->model();
if (model->rowCount() > 0) {
ui_->results->selectionModel()->setCurrentIndex(model->index(0, 0), QItemSelectionModel::ClearAndSelect);
}
}
void AddPodcastDialog::AddPodcast() {
app_->podcast_backend()->Subscribe(&current_podcast_);
add_button_->setEnabled(false);
remove_button_->setEnabled(true);
}
void AddPodcastDialog::PodcastDoubleClicked(const QModelIndex &idx) {
QVariant podcast_variant = idx.data(PodcastDiscoveryModel::Role_Podcast);
if (podcast_variant.isNull()) {
return;
}
current_podcast_ = podcast_variant.value<Podcast>();
app_->podcast_backend()->Subscribe(&current_podcast_);
add_button_->setEnabled(false);
remove_button_->setEnabled(true);
}
void AddPodcastDialog::RemovePodcast() {
app_->podcast_backend()->Unsubscribe(current_podcast_);
current_podcast_.set_database_id(-1);
add_button_->setEnabled(true);
remove_button_->setEnabled(false);
}
void AddPodcastDialog::OpenSettingsPage() {
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
}
void AddPodcastDialog::OpenOPMLFile() {
const QString filename = QFileDialog::getOpenFileName(this, tr("Open OPML file"), last_opml_path_, "OPML files (*.opml)");
if (filename.isEmpty()) {
return;
}
last_opml_path_ = filename;
by_url_page_->SetUrlAndGo(QUrl::fromLocalFile(last_opml_path_));
ChangePage(ui_->stack->indexOf(by_url_page_));
}

View File

@ -0,0 +1,91 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ADDPODCASTDIALOG_H
#define ADDPODCASTDIALOG_H
#include <QDialog>
#include <QList>
#include <QString>
#include <QUrl>
#include "podcast.h"
class Application;
class AddPodcastByUrl;
class AddPodcastPage;
class OpmlContainer;
class WidgetFadeHelper;
class Ui_AddPodcastDialog;
class AddPodcastDialog : public QDialog {
Q_OBJECT
public:
explicit AddPodcastDialog(Application *app, QWidget *parent = nullptr);
~AddPodcastDialog();
// Convenience methods that open the dialog at the Add By Url page and fill it with either a URL (which is then fetched), or a pre-fetched OPML container.
void ShowWithUrl(const QUrl &url);
void ShowWithOpml(const OpmlContainer &opml);
private slots:
void OpenSettingsPage();
void AddPodcast();
void PodcastDoubleClicked(const QModelIndex &idx);
void RemovePodcast();
void ChangePage(const int index);
void ChangePodcast(const QModelIndex &current);
void PageBusyChanged(const bool busy);
void CurrentPageBusyChanged(const bool busy);
void SelectFirstPodcast();
void OpenOPMLFile();
private:
void AddPage(AddPodcastPage *page);
private:
static const char *kBbcOpmlUrl;
static const char *kCbcOpmlUrl;
Application *app_;
Ui_AddPodcastDialog *ui_;
QPushButton *add_button_;
QPushButton *remove_button_;
QList<AddPodcastPage*> pages_;
QList<bool> page_is_busy_;
AddPodcastByUrl *by_url_page_;
WidgetFadeHelper *fader_;
Podcast current_podcast_;
QString last_opml_path_;
};
#endif // ADDPODCASTDIALOG_H

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastDialog</class>
<widget class="QDialog" name="AddPodcastDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>941</width>
<height>473</height>
</rect>
</property>
<property name="windowTitle">
<string>Add podcast</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="provider_list">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="results_stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="results_page">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="AutoExpandingTreeView" name="results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="busy_page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>192</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="BusyIndicator" name="widget" native="true">
<property name="text" stdset="0">
<string>Loading...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="details_scroll_area">
<property name="minimumSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="PodcastInfoWidget" name="details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>248</width>
<height>415</height>
</rect>
</property>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PodcastInfoWidget</class>
<extends>QWidget</extends>
<header>podcasts/podcastinfowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>
<header>widgets/autoexpandingtreeview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>836</x>
<y>463</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>885</x>
<y>463</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,36 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QWidget>
#include "addpodcastpage.h"
#include "podcastdiscoverymodel.h"
AddPodcastPage::AddPodcastPage(Application *app, QWidget *parent)
: QWidget(parent), model_(new PodcastDiscoveryModel(app, this)) {}
void AddPodcastPage::SetModel(PodcastDiscoveryModel *model) {
delete model_;
model_ = model;
}

View File

@ -0,0 +1,53 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ADDPODCASTPAGE_H
#define ADDPODCASTPAGE_H
#include <QWidget>
class Application;
class PodcastDiscoveryModel;
class AddPodcastPage : public QWidget {
Q_OBJECT
public:
explicit AddPodcastPage(Application *app, QWidget *parent = nullptr);
PodcastDiscoveryModel *model() const { return model_; }
virtual bool has_visible_widget() const { return true; }
virtual void Show() {}
signals:
void Busy(bool busy);
protected:
void SetModel(PodcastDiscoveryModel *model);
private:
PodcastDiscoveryModel *model_;
};
#endif // ADDPODCASTPAGE_H

View File

@ -0,0 +1,49 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QWidget>
#include "core/utilities.h"
#include "episodeinfowidget.h"
#include "ui_episodeinfowidget.h"
EpisodeInfoWidget::EpisodeInfoWidget(QWidget *parent)
: QWidget(parent), ui_(new Ui_EpisodeInfoWidget), app_(nullptr) {
ui_->setupUi(this);
}
EpisodeInfoWidget::~EpisodeInfoWidget() { delete ui_; }
void EpisodeInfoWidget::SetApplication(Application *app) { app_ = app; }
void EpisodeInfoWidget::SetEpisode(const PodcastEpisode &episode) {
episode_ = episode;
ui_->title->setText(episode.title());
ui_->description->setText(episode.description());
ui_->author->setText(episode.author());
ui_->date->setText(episode.publication_date().toString("d MMMM yyyy"));
ui_->duration->setText(Utilities::PrettyTime(episode.duration_secs()));
}

View File

@ -0,0 +1,50 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef EPISODEINFOWIDGET_H
#define EPISODEINFOWIDGET_H
#include <QWidget>
#include "podcastepisode.h"
class Application;
class Ui_EpisodeInfoWidget;
class EpisodeInfoWidget : public QWidget {
Q_OBJECT
public:
explicit EpisodeInfoWidget(QWidget *parent = nullptr);
~EpisodeInfoWidget();
void SetApplication(Application *app);
void SetEpisode(const PodcastEpisode &episode);
private:
Ui_EpisodeInfoWidget *ui_;
Application *app_;
PodcastEpisode episode_;
};
#endif // EPISODEINFOWIDGET_H

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EpisodeInfoWidget</class>
<widget class="QWidget" name="EpisodeInfoWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>551</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#title {
font-weight: bold;
}
#description {
font-size: smaller;
}
QLineEdit {
background: transparent;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<widget class="QLabel" name="title">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="description">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="date">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="author">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="date_label">
<property name="text">
<string>Date</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="duration_label">
<property name="text">
<string>Duration</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="duration">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,82 @@
/*
</