diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3b4eb1f0..679fe896 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,8 @@ if(ANDROID) overflow-menu checkbox error + search + kt-add-feeds ) else() target_link_libraries(kasts PRIVATE Qt::Widgets Qt::DBus) diff --git a/src/datamanager.cpp b/src/datamanager.cpp index 8eac6b58..05bfa2e2 100644 --- a/src/datamanager.cpp +++ b/src/datamanager.cpp @@ -626,3 +626,8 @@ void DataManager::updateQueueListnrs() const Database::instance().execute(query); } } + +bool DataManager::isFeedExists(const QString &url) +{ + return m_feeds.contains(url); +} diff --git a/src/datamanager.h b/src/datamanager.h index 367daa57..921dceb6 100644 --- a/src/datamanager.h +++ b/src/datamanager.h @@ -59,6 +59,7 @@ public: Q_INVOKABLE void importFeeds(const QString &path); Q_INVOKABLE void exportFeeds(const QString &path); + Q_INVOKABLE bool isFeedExists(const QString &url); Q_SIGNALS: void feedAdded(const QString &url); diff --git a/src/podcastsearchmodel.cpp b/src/podcastsearchmodel.cpp index 9228156c..042d7a30 100644 --- a/src/podcastsearchmodel.cpp +++ b/src/podcastsearchmodel.cpp @@ -30,15 +30,95 @@ QVariant PodcastSearchModel::data(const QModelIndex &index, int role) const // invalid index return QVariant::fromValue(QStringLiteral("DEADBEEF")); } - if (role == Title) { + switch (role) { + case Id: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("id")].toInt(); + case Title: return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("title")].toString(); + case Url: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("url")].toString(); + case OriginalUrl: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("originalUrl")].toString(); + case Link: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("link")].toString(); + case Description: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("description")].toString(); + case Author: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("author")].toString(); + case OwnerName: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("ownerName")].toString(); + case Image: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("image")].toString(); + case Artwork: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("artwork")].toString(); + case LastUpdateTime: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("lastUpdateTime")].toInt(); + case LastCrawlTime: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("lastCrawlTime")].toInt(); + case LastParseTime: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("lastParseTime")].toInt(); + case LastGoodHttpStatusTime: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("lastGoodHttpStatusTime")].toInt(); + case LastHttpStatus: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("lastHttpStatus")].toInt(); + case ContentType: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("contentType")].toString(); + case ItunesId: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("itunesId")].toInt(); + case Generator: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("generator")].toString(); + case Language: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("language")].toString(); + case Type: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("type")].toInt(); + case Dead: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("dead")].toInt(); + case CrawlErrors: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("crawlErrors")].toInt(); + case ParseErrors: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("parseErrors")].toInt(); + case Categories: { + // TODO: Implement this function to add to the list of categories. + } + case Locked: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("locked")].toInt(); + case ImageUrlHash: + return m_data[QStringLiteral("feeds")].toArray()[index.row()].toObject()[QStringLiteral("imageUrlHash")].toInt(); + default: + return QVariant(); } - return QVariant(); } QHash PodcastSearchModel::roleNames() const { - return {{Title, "title"}}; + return { + {Id, "id"}, + {Title, "title"}, + {Url, "url"}, + {OriginalUrl, "originalUrl"}, + {Link, "link"}, + {Description, "description"}, + {Author, "author"}, + {OwnerName, "ownerName"}, + {Image, "image"}, + {Artwork, "artwork"}, + {LastUpdateTime, "lastUpdateTime"}, + {LastCrawlTime, "lastCrawlTime"}, + {LastParseTime, "lastParseTime"}, + {LastGoodHttpStatusTime, "lastGoodHttpStatusTime"}, + {LastHttpStatus, "lastHttpStatus"}, + {ContentType, "contentType"}, + {ItunesId, "itunesId"}, + {Generator, "generator"}, + {Language, "language"}, + {Type, "type"}, + {Dead, "dead"}, + {CrawlErrors, "crawlErrors"}, + {ParseErrors, "parseErrors"}, + {Categories, "categories"}, + {Locked, "locked"}, + {ImageUrlHash, "imageUrlHash"}, + }; } int PodcastSearchModel::rowCount(const QModelIndex &parent) const @@ -47,7 +127,8 @@ int PodcastSearchModel::rowCount(const QModelIndex &parent) const if (m_data.isEmpty()) { return 0; } - return m_data[QStringLiteral("feeds")].toArray().size(); + return m_data[QStringLiteral("feeds")].toArray().size(); + } void PodcastSearchModel::search(const QString &text) diff --git a/src/podcastsearchmodel.h b/src/podcastsearchmodel.h index 4d18a875..59b202de 100644 --- a/src/podcastsearchmodel.h +++ b/src/podcastsearchmodel.h @@ -17,10 +17,34 @@ class PodcastSearchModel : public QAbstractListModel { Q_OBJECT - public: enum Roles { + Id, Title, + Url, + OriginalUrl, + Link, + Description, + Author, + OwnerName, + Image, + Artwork, + LastUpdateTime, + LastCrawlTime, + LastParseTime, + LastGoodHttpStatusTime, + LastHttpStatus, + ContentType, + ItunesId, + Generator, + Language, + Type, + Dead, + CrawlErrors, + ParseErrors, + Categories, + Locked, + ImageUrlHash, }; explicit PodcastSearchModel(QObject *parent = nullptr); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; diff --git a/src/qml/DiscoverPage.qml b/src/qml/DiscoverPage.qml new file mode 100644 index 00000000..139534fb --- /dev/null +++ b/src/qml/DiscoverPage.qml @@ -0,0 +1,119 @@ +/** + * SPDX-FileCopyrightText: 2021 Swapnil Tripathi + * + * 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.15 as Controls +import QtQuick.Layouts 1.14 + +import org.kde.kirigami 2.15 as Kirigami +import org.kde.kasts 1.0 + +Kirigami.ScrollablePage { + id: page + title: i18n("Discover") + property var feedModel: "" + header: RowLayout { + width: parent.width + anchors.topMargin: Kirigami.Units.smallSpacing + Kirigami.SearchField { + id: textField + placeholderText: i18n("Search podcastindex.org") + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing + } + Controls.Button { + id: searchButton + text: isWidescreen ? i18n("Search") : "" + icon.name: "search" + Layout.rightMargin: Kirigami.Units.smallSpacing + onClicked: { + podcastSearchModel.search(textField.text); + } + } + } + Component.onCompleted: { + textField.forceActiveFocus(); + } + Component { + id: delegateComponent + Kirigami.SwipeListItem { + id: listItem + contentItem: RowLayout { + Kirigami.Icon { + source: model.image + Layout.fillHeight: true + Layout.maximumHeight: Kirigami.Units.iconSizes.huge + Layout.preferredWidth: height + } + Controls.Label { + Layout.fillWidth: true + height: Math.max(implicitHeight, Kirigami.Units.iconSizes.smallMedium) + text: model.title + elide: Text.ElideRight + color: listItem.checked || (listItem.pressed && !listItem.checked && !listItem.sectionDelegate) ? listItem.activeTextColor : listItem.textColor + } + } + actions: [ + Kirigami.Action { + text: enabled ? i18n("Subscribe") : i18n("Subscribed") + icon.name: "kt-add-feeds" + enabled: !DataManager.isFeedExists(model.url) + onTriggered: { + DataManager.addFeed(model.url) + } + } + ] + onClicked: { + feedModel = model + detailDrawer.open(); + } + } + } + ListView { + anchors.fill: parent + model: PodcastSearchModel { + id: podcastSearchModel + } + spacing: 5 + clip: true + delegate: Kirigami.DelegateRecycler { + width: parent ? parent.width : implicitWidth + sourceComponent: delegateComponent + } + } + Kirigami.OverlaySheet { + id: detailDrawer + showCloseButton: true + contentItem: ColumnLayout { + Layout.preferredWidth: Kirigami.Units.gridUnit * 25 + GenericHeader { + image: feedModel.image + title: feedModel.title + subtitle: feedModel.author + Controls.Button { + text: enabled ? "Subscribe" : "Subscribed" + icon.name: "kt-add-feeds" + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: Kirigami.Units.largeSpacing + anchors.topMargin: Kirigami.Units.largeSpacing + onClicked: { + DataManager.addFeed(feedModel.url) + } + enabled: !DataManager.isFeedExists(feedModel.url) + } + } + Controls.Label { + text: i18n("%1 \n\nAuthor: %2 \nOwner: %3", feedModel.description, feedModel.author, feedModel.ownerName) + Layout.margins: Kirigami.Units.gridUnit + wrapMode: Text.WordWrap + Layout.fillWidth: true + onLinkActivated: Qt.openUrlExternally(link) + font.pointSize: SettingsManager && !(SettingsManager.articleFontUseSystem) ? SettingsManager.articleFontSize : Kirigami.Units.fontMetrics.font.pointSize + } + } + } +} diff --git a/src/qml/main.qml b/src/qml/main.qml index 092e43e3..8c1f8792 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -38,6 +38,7 @@ Kirigami.ApplicationWindow { switch (page) { case "QueuePage": return "qrc:/QueuePage.qml"; case "EpisodeSwipePage": return "qrc:/EpisodeSwipePage.qml"; + case "DiscoverPage": return "qrc:/DiscoverPage.qml"; case "FeedListPage": return "qrc:/FeedListPage.qml"; case "DownloadListPage": return "qrc:/DownloadListPage.qml"; case "SettingsPage": return "qrc:/SettingsPage.qml"; @@ -56,6 +57,7 @@ Kirigami.ApplicationWindow { : SettingsManager.lastOpenedPage === "QueuePage" ? 0 : SettingsManager.lastOpenedPage === "EpisodeSwipePage" ? 1 : SettingsManager.lastOpenedPage === "DownloadListPage" ? 0 + : SettingsManager.lastOpenedPage === "DiscoverPage" ? 0 : 0 currentPage = SettingsManager.lastOpenedPage pageStack.initialPage = getPage(SettingsManager.lastOpenedPage) @@ -103,6 +105,16 @@ Kirigami.ApplicationWindow { tabBarActive = 0 } }, + Kirigami.Action { + text: i18n("Discover") + iconName: "search" + checked: currentPage == "DiscoverPage" + onTriggered: { + pushPage("DiscoverPage") + SettingsManager.lastOpenedPage = "DiscoverPage" // for persistency + tabBarActive = 0 + } + }, Kirigami.Action { text: i18n("Episodes") iconName: "rss" diff --git a/src/resources.qrc b/src/resources.qrc index 69f4ab87..47d120cb 100755 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -21,6 +21,7 @@ qml/EpisodeSwipePage.qml qml/GenericHeader.qml qml/GenericEntryDelegate.qml + qml/DiscoverPage.qml qml/ImageWithFallback.qml qml/UpdateNotification.qml qml/HeaderBar.qml