mirror of https://github.com/KDE/kasts.git
Implement episode search functionality
Add search capabilities to the main window toolbar and the episode lists. BUG: 459983
This commit is contained in:
parent
3e6df78991
commit
e5e046e412
|
@ -52,7 +52,7 @@ ecm_set_disabled_deprecation_versions(
|
|||
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui QuickControls2 Sql Svg)
|
||||
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n CoreAddons Kirigami2 Syndication Config ThreadWeaver)
|
||||
find_package(KF5KirigamiAddons 0.6 REQUIRED)
|
||||
find_package(KF5KirigamiAddons 0.7 REQUIRED)
|
||||
find_package(Taglib REQUIRED)
|
||||
find_package(Qt${QT_MAJOR_VERSION}Keychain)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Keychain PROPERTIES
|
||||
|
|
|
@ -23,10 +23,13 @@ set(kasts_srcs
|
|||
updatefeedjob.cpp
|
||||
fetchfeedsjob.cpp
|
||||
systrayicon.cpp
|
||||
models/abstractepisodemodel.cpp
|
||||
models/abstractepisodeproxymodel.cpp
|
||||
models/chaptermodel.cpp
|
||||
models/feedsmodel.cpp
|
||||
models/feedsproxymodel.cpp
|
||||
models/entriesmodel.cpp
|
||||
models/entriesproxymodel.cpp
|
||||
models/queuemodel.cpp
|
||||
models/episodemodel.cpp
|
||||
models/episodeproxymodel.cpp
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
#include "feed.h"
|
||||
#include "feedlogging.h"
|
||||
#include "fetcher.h"
|
||||
#include "models/entriesmodel.h"
|
||||
|
||||
Feed::Feed(const QString &feedurl)
|
||||
: QObject(&DataManager::instance())
|
||||
|
@ -78,7 +77,7 @@ Feed::Feed(const QString &feedurl)
|
|||
}
|
||||
});
|
||||
|
||||
m_entries = new EntriesModel(this);
|
||||
m_entries = new EntriesProxyModel(this);
|
||||
}
|
||||
|
||||
Feed::~Feed()
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#include <QVector>
|
||||
|
||||
#include "author.h"
|
||||
#include "models/entriesmodel.h"
|
||||
#include "models/entriesproxymodel.h"
|
||||
|
||||
class Feed : public QObject
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ class Feed : public QObject
|
|||
Q_PROPERTY(int newEntryCount READ newEntryCount NOTIFY newEntryCountChanged)
|
||||
Q_PROPERTY(int errorId READ errorId WRITE setErrorId NOTIFY errorIdChanged)
|
||||
Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged)
|
||||
Q_PROPERTY(EntriesModel *entries MEMBER m_entries CONSTANT)
|
||||
Q_PROPERTY(EntriesProxyModel *entries MEMBER m_entries CONSTANT)
|
||||
|
||||
public:
|
||||
Feed(const QString &feedurl);
|
||||
|
@ -120,7 +120,7 @@ private:
|
|||
QString m_errorString;
|
||||
int m_unreadEntryCount = -1;
|
||||
|
||||
EntriesModel *m_entries;
|
||||
EntriesProxyModel *m_entries;
|
||||
|
||||
bool m_refreshing = false;
|
||||
};
|
||||
|
|
12
src/main.cpp
12
src/main.cpp
|
@ -39,10 +39,11 @@
|
|||
#include "feed.h"
|
||||
#include "fetcher.h"
|
||||
#include "kasts-version.h"
|
||||
#include "models/abstractepisodemodel.h"
|
||||
#include "models/abstractepisodeproxymodel.h"
|
||||
#include "models/chaptermodel.h"
|
||||
#include "models/downloadmodel.h"
|
||||
#include "models/entriesmodel.h"
|
||||
#include "models/episodemodel.h"
|
||||
#include "models/entriesproxymodel.h"
|
||||
#include "models/episodeproxymodel.h"
|
||||
#include "models/errorlogmodel.h"
|
||||
#include "models/feedsproxymodel.h"
|
||||
|
@ -118,7 +119,7 @@ int main(int argc, char *argv[])
|
|||
QStringLiteral(KASTS_VERSION_STRING),
|
||||
i18n("Podcast Player"),
|
||||
KAboutLicense::GPL,
|
||||
i18n("© 2020-2022 KDE Community"));
|
||||
i18n("© 2020-2023 KDE Community"));
|
||||
about.addAuthor(i18n("Tobias Fella"), QString(), QStringLiteral("fella@posteo.de"), QStringLiteral("https://tobiasfella.de"));
|
||||
about.addAuthor(i18n("Bart De Vries"), QString(), QStringLiteral("bart@mogwai.be"));
|
||||
about.setProgramLogo(QVariant(QIcon(QStringLiteral(":/logo.svg"))));
|
||||
|
@ -139,9 +140,10 @@ int main(int argc, char *argv[])
|
|||
qmlRegisterType<PodcastSearchModel>("org.kde.kasts", 1, 0, "PodcastSearchModel");
|
||||
qmlRegisterType<ChapterModel>("org.kde.kasts", 1, 0, "ChapterModel");
|
||||
|
||||
qmlRegisterUncreatableType<EntriesModel>("org.kde.kasts", 1, 0, "EntriesModel", QStringLiteral("Get from Feed"));
|
||||
qmlRegisterUncreatableType<AbstractEpisodeProxyModel>("org.kde.kasts", 1, 0, "AbstractEpisodeProxyModel", QStringLiteral("Only for enums"));
|
||||
qmlRegisterUncreatableType<EntriesProxyModel>("org.kde.kasts", 1, 0, "EntriesProxyModel", QStringLiteral("Get from Feed"));
|
||||
qmlRegisterUncreatableType<Enclosure>("org.kde.kasts", 1, 0, "Enclosure", QStringLiteral("Only for enums"));
|
||||
qmlRegisterUncreatableType<EpisodeModel>("org.kde.kasts", 1, 0, "EpisodeModel", QStringLiteral("Only for enums"));
|
||||
qmlRegisterUncreatableType<AbstractEpisodeModel>("org.kde.kasts", 1, 0, "AbstractEpisodeModel", QStringLiteral("Only for enums"));
|
||||
qmlRegisterUncreatableType<FeedsModel>("org.kde.kasts", 1, 0, "FeedsModel", QStringLiteral("Only for enums"));
|
||||
|
||||
qmlRegisterSingletonType("org.kde.kasts", 1, 0, "About", [](QQmlEngine *engine, QJSEngine *) -> QJSValue {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#include "models/abstractepisodemodel.h"
|
||||
|
||||
AbstractEpisodeModel::AbstractEpisodeModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AbstractEpisodeModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{TitleRole, "title"},
|
||||
{EntryRole, "entry"},
|
||||
{IdRole, "id"},
|
||||
{ReadRole, "read"},
|
||||
{NewRole, "new"},
|
||||
{ContentRole, "content"},
|
||||
{FeedNameRole, "feedname"},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QByteArray>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
|
||||
class AbstractEpisodeModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
TitleRole = Qt::DisplayRole,
|
||||
EntryRole = Qt::UserRole + 1,
|
||||
IdRole,
|
||||
ReadRole,
|
||||
NewRole,
|
||||
ContentRole,
|
||||
FeedNameRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit AbstractEpisodeModel(QObject *parent = nullptr);
|
||||
virtual QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
public Q_SLOTS:
|
||||
virtual void updateInternalState() = 0;
|
||||
};
|
|
@ -0,0 +1,178 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#include "models/episodeproxymodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
#include "models/abstractepisodemodel.h"
|
||||
|
||||
AbstractEpisodeProxyModel::AbstractEpisodeProxyModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
m_searchFlags = SearchFlag::TitleFlag | SearchFlag::ContentFlag | SearchFlag::FeedNameFlag;
|
||||
}
|
||||
|
||||
bool AbstractEpisodeProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
|
||||
bool accepted = true;
|
||||
|
||||
switch (m_currentFilter) {
|
||||
case NoFilter:
|
||||
accepted = true;
|
||||
break;
|
||||
case ReadFilter:
|
||||
accepted = sourceModel()->data(index, AbstractEpisodeModel::Roles::ReadRole).value<bool>();
|
||||
break;
|
||||
case NotReadFilter:
|
||||
accepted = !sourceModel()->data(index, AbstractEpisodeModel::Roles::ReadRole).value<bool>();
|
||||
break;
|
||||
case NewFilter:
|
||||
accepted = sourceModel()->data(index, AbstractEpisodeModel::Roles::NewRole).value<bool>();
|
||||
break;
|
||||
case NotNewFilter:
|
||||
accepted = !sourceModel()->data(index, AbstractEpisodeModel::Roles::NewRole).value<bool>();
|
||||
break;
|
||||
default:
|
||||
accepted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
bool found = m_searchFilter.isEmpty();
|
||||
if (!m_searchFilter.isEmpty()) {
|
||||
if (m_searchFlags & SearchFlag::TitleFlag) {
|
||||
if (sourceModel()->data(index, AbstractEpisodeModel::Roles::TitleRole).value<QString>().contains(m_searchFilter, Qt::CaseInsensitive)) {
|
||||
found |= true;
|
||||
}
|
||||
}
|
||||
if (m_searchFlags & SearchFlag::ContentFlag) {
|
||||
if (sourceModel()->data(index, AbstractEpisodeModel::Roles::ContentRole).value<QString>().contains(m_searchFilter, Qt::CaseInsensitive)) {
|
||||
found |= true;
|
||||
}
|
||||
}
|
||||
if (m_searchFlags & SearchFlag::FeedNameFlag) {
|
||||
if (sourceModel()->data(index, AbstractEpisodeModel::Roles::FeedNameRole).value<QString>().contains(m_searchFilter, Qt::CaseInsensitive)) {
|
||||
found |= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accepted = accepted && found;
|
||||
|
||||
return accepted;
|
||||
}
|
||||
|
||||
AbstractEpisodeProxyModel::FilterType AbstractEpisodeProxyModel::filterType() const
|
||||
{
|
||||
return m_currentFilter;
|
||||
}
|
||||
|
||||
QString AbstractEpisodeProxyModel::filterName() const
|
||||
{
|
||||
return getFilterName(m_currentFilter);
|
||||
}
|
||||
|
||||
QString AbstractEpisodeProxyModel::searchFilter() const
|
||||
{
|
||||
return m_searchFilter;
|
||||
}
|
||||
|
||||
AbstractEpisodeProxyModel::SearchFlags AbstractEpisodeProxyModel::searchFlags() const
|
||||
{
|
||||
return m_searchFlags;
|
||||
}
|
||||
|
||||
void AbstractEpisodeProxyModel::setFilterType(FilterType type)
|
||||
{
|
||||
if (type != m_currentFilter) {
|
||||
disconnect(&DataManager::instance(), &DataManager::bulkReadStatusActionFinished, this, nullptr);
|
||||
disconnect(&DataManager::instance(), &DataManager::bulkNewStatusActionFinished, this, nullptr);
|
||||
|
||||
beginResetModel();
|
||||
m_currentFilter = type;
|
||||
dynamic_cast<AbstractEpisodeModel *>(sourceModel())->updateInternalState();
|
||||
endResetModel();
|
||||
|
||||
// connect to signals which indicate that read/new statuses have been updated
|
||||
if (type == ReadFilter || type == NotReadFilter) {
|
||||
connect(&DataManager::instance(), &DataManager::bulkReadStatusActionFinished, this, [this]() {
|
||||
beginResetModel();
|
||||
dynamic_cast<AbstractEpisodeModel *>(sourceModel())->updateInternalState();
|
||||
endResetModel();
|
||||
});
|
||||
} else if (type == NewFilter || type == NotNewFilter) {
|
||||
connect(&DataManager::instance(), &DataManager::bulkNewStatusActionFinished, this, [this]() {
|
||||
beginResetModel();
|
||||
dynamic_cast<AbstractEpisodeModel *>(sourceModel())->updateInternalState();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
Q_EMIT filterTypeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractEpisodeProxyModel::setSearchFilter(const QString &searchString)
|
||||
{
|
||||
if (searchString != m_searchFilter) {
|
||||
beginResetModel();
|
||||
m_searchFilter = searchString;
|
||||
endResetModel();
|
||||
|
||||
Q_EMIT searchFilterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractEpisodeProxyModel::setSearchFlags(AbstractEpisodeProxyModel::SearchFlags searchFlags)
|
||||
{
|
||||
if (searchFlags != m_searchFlags) {
|
||||
beginResetModel();
|
||||
m_searchFlags = searchFlags;
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
QString AbstractEpisodeProxyModel::getFilterName(FilterType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case FilterType::NoFilter:
|
||||
return i18nc("@label:chooser Choice of filter for episode list", "No Filter");
|
||||
case FilterType::ReadFilter:
|
||||
return i18nc("@label:chooser Choice of filter for episode list", "Played Episodes");
|
||||
case FilterType::NotReadFilter:
|
||||
return i18nc("@label:chooser Choice of filter for episode list", "Unplayed Episodes");
|
||||
case FilterType::NewFilter:
|
||||
return i18nc("@label:chooser Choice of filter for episode list", "Episodes marked as \"New\"");
|
||||
case FilterType::NotNewFilter:
|
||||
return i18nc("@label:chooser Choice of filter for episode list", "Episodes not marked as \"New\"");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString AbstractEpisodeProxyModel::getSearchFlagName(SearchFlag flag) const
|
||||
{
|
||||
switch (flag) {
|
||||
case SearchFlag::TitleFlag:
|
||||
return i18nc("@label:chooser Choice of fields to search for string", "Title");
|
||||
case SearchFlag::ContentFlag:
|
||||
return i18nc("@label:chooser Choice of fields to search for string", "Description");
|
||||
case SearchFlag::FeedNameFlag:
|
||||
return i18nc("@label:chooser Choice of fields to search for string", "Podcast Title");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection AbstractEpisodeProxyModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QItemSelection>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
class Entry;
|
||||
|
||||
class AbstractEpisodeProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum FilterType {
|
||||
NoFilter,
|
||||
ReadFilter,
|
||||
NotReadFilter,
|
||||
NewFilter,
|
||||
NotNewFilter,
|
||||
};
|
||||
Q_ENUM(FilterType)
|
||||
|
||||
enum SearchFlag {
|
||||
TitleFlag = 0x01,
|
||||
ContentFlag = 0x02,
|
||||
FeedNameFlag = 0x04,
|
||||
};
|
||||
Q_ENUM(SearchFlag)
|
||||
Q_DECLARE_FLAGS(SearchFlags, SearchFlag)
|
||||
Q_FLAGS(SearchFlags)
|
||||
|
||||
Q_PROPERTY(FilterType filterType READ filterType WRITE setFilterType NOTIFY filterTypeChanged)
|
||||
Q_PROPERTY(QString filterName READ filterName NOTIFY filterTypeChanged)
|
||||
Q_PROPERTY(QString searchFilter READ searchFilter WRITE setSearchFilter NOTIFY searchFilterChanged)
|
||||
Q_PROPERTY(SearchFlags searchFlags READ searchFlags WRITE setSearchFlags NOTIFY searchFlagsChanged)
|
||||
|
||||
explicit AbstractEpisodeProxyModel(QObject *parent = nullptr);
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
FilterType filterType() const;
|
||||
QString filterName() const;
|
||||
QString searchFilter() const;
|
||||
SearchFlags searchFlags() const;
|
||||
|
||||
void setFilterType(FilterType type);
|
||||
void setSearchFilter(const QString &searchString);
|
||||
void setSearchFlags(SearchFlags searchFlags);
|
||||
|
||||
Q_INVOKABLE QString getFilterName(FilterType type) const;
|
||||
Q_INVOKABLE QString getSearchFlagName(SearchFlag flag) const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTypeChanged();
|
||||
void searchFilterChanged();
|
||||
void searchFlagsChanged();
|
||||
|
||||
protected:
|
||||
FilterType m_currentFilter = FilterType::NoFilter;
|
||||
QString m_searchFilter;
|
||||
SearchFlags m_searchFlags;
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(AbstractEpisodeProxyModel::SearchFlags)
|
|
@ -12,10 +12,9 @@
|
|||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
#include "feed.h"
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
EntriesModel::EntriesModel(Feed *feed)
|
||||
: QAbstractListModel(feed)
|
||||
: AbstractEpisodeModel(feed)
|
||||
, m_feed(feed)
|
||||
{
|
||||
// When feed is updated, the entire model needs to be reset because we
|
||||
|
@ -31,27 +30,27 @@ EntriesModel::EntriesModel(Feed *feed)
|
|||
|
||||
QVariant EntriesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
Entry *entry = DataManager::instance().getEntry(m_feed, index.row());
|
||||
switch (role) {
|
||||
case EpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_feed, index.row()));
|
||||
case Qt::DisplayRole:
|
||||
case EpisodeModel::Roles::IdRole:
|
||||
case AbstractEpisodeModel::Roles::TitleRole:
|
||||
return QVariant::fromValue(entry->title());
|
||||
case AbstractEpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(entry);
|
||||
case AbstractEpisodeModel::Roles::IdRole:
|
||||
return QVariant::fromValue(DataManager::instance().getIdList(m_feed)[index.row()]);
|
||||
case AbstractEpisodeModel::Roles::ReadRole:
|
||||
return QVariant::fromValue(entry->read());
|
||||
case AbstractEpisodeModel::Roles::NewRole:
|
||||
return QVariant::fromValue(entry->getNew());
|
||||
case AbstractEpisodeModel::Roles::ContentRole:
|
||||
return QVariant::fromValue(entry->content());
|
||||
case AbstractEpisodeModel::Roles::FeedNameRole:
|
||||
return QVariant::fromValue(m_feed->name());
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> EntriesModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{EpisodeModel::Roles::EntryRole, "entry"},
|
||||
{EpisodeModel::Roles::IdRole, "id"},
|
||||
{EpisodeModel::Roles::ReadRole, "read"},
|
||||
{EpisodeModel::Roles::NewRole, "new"},
|
||||
};
|
||||
}
|
||||
|
||||
int EntriesModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
|
@ -63,8 +62,7 @@ Feed *EntriesModel::feed() const
|
|||
return m_feed;
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection EntriesModel::createSelection(int rowa, int rowb)
|
||||
void EntriesModel::updateInternalState()
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
// nothing to do; DataManager already has the updated data.
|
||||
}
|
||||
|
|
|
@ -1,36 +1,35 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QItemSelection>
|
||||
#include <QModelIndex>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
#include "models/abstractepisodemodel.h"
|
||||
|
||||
class Feed;
|
||||
|
||||
class EntriesModel : public QAbstractListModel
|
||||
class EntriesModel : public AbstractEpisodeModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(Feed *feed READ feed CONSTANT)
|
||||
|
||||
public:
|
||||
explicit EntriesModel(Feed *feed);
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
Feed *feed() const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
public Q_SLOTS:
|
||||
void updateInternalState() override;
|
||||
|
||||
private:
|
||||
protected:
|
||||
Feed *m_feed;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#include "models/entriesproxymodel.h"
|
||||
|
||||
#include <QModelIndex>
|
||||
#include <QString>
|
||||
|
||||
#include "feed.h"
|
||||
#include "models/entriesmodel.h"
|
||||
|
||||
EntriesProxyModel::EntriesProxyModel(Feed *feed)
|
||||
: AbstractEpisodeProxyModel(feed)
|
||||
{
|
||||
m_entriesModel = new EntriesModel(feed);
|
||||
setSourceModel(m_entriesModel);
|
||||
}
|
||||
|
||||
Feed *EntriesProxyModel::feed() const
|
||||
{
|
||||
return m_entriesModel->feed();
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "models/abstractepisodeproxymodel.h"
|
||||
#include "models/entriesmodel.h"
|
||||
|
||||
class Feed;
|
||||
|
||||
class EntriesProxyModel : public AbstractEpisodeProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(Feed *feed READ feed CONSTANT)
|
||||
|
||||
public:
|
||||
explicit EntriesProxyModel(Feed *feed);
|
||||
|
||||
Feed *feed() const;
|
||||
|
||||
private:
|
||||
EntriesModel *m_entriesModel;
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
@ -13,7 +13,7 @@
|
|||
#include "entry.h"
|
||||
|
||||
EpisodeModel::EpisodeModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractEpisodeModel(parent)
|
||||
{
|
||||
// When feed is updated, the entire model needs to be reset because we
|
||||
// cannot know where the new entries will be inserted into the list (or that
|
||||
|
@ -31,29 +31,25 @@ EpisodeModel::EpisodeModel(QObject *parent)
|
|||
QVariant EpisodeModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case EntryRole:
|
||||
case AbstractEpisodeModel::Roles::TitleRole:
|
||||
return QVariant::fromValue(m_titles[index.row()]);
|
||||
case AbstractEpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_entryIds[index.row()]));
|
||||
case IdRole:
|
||||
case AbstractEpisodeModel::Roles::IdRole:
|
||||
return QVariant::fromValue(m_entryIds[index.row()]);
|
||||
case ReadRole:
|
||||
case AbstractEpisodeModel::Roles::ReadRole:
|
||||
return QVariant::fromValue(m_read[index.row()]);
|
||||
case NewRole:
|
||||
case AbstractEpisodeModel::Roles::NewRole:
|
||||
return QVariant::fromValue(m_new[index.row()]);
|
||||
case AbstractEpisodeModel::Roles::ContentRole:
|
||||
return QVariant::fromValue(m_contents[index.row()]);
|
||||
case AbstractEpisodeModel::Roles::FeedNameRole:
|
||||
return QVariant::fromValue(m_feedNames[index.row()]);
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> EpisodeModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{EntryRole, "entry"},
|
||||
{IdRole, "id"},
|
||||
{ReadRole, "read"},
|
||||
{NewRole, "new"},
|
||||
};
|
||||
}
|
||||
|
||||
int EpisodeModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
|
@ -65,12 +61,18 @@ void EpisodeModel::updateInternalState()
|
|||
m_entryIds.clear();
|
||||
m_read.clear();
|
||||
m_new.clear();
|
||||
m_titles.clear();
|
||||
m_feedNames.clear();
|
||||
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("SELECT id, read, new FROM Entries ORDER BY updated DESC;"));
|
||||
query.prepare(QStringLiteral("SELECT id, read, new, title, content, feed FROM Entries ORDER BY updated DESC;"));
|
||||
Database::instance().execute(query);
|
||||
while (query.next()) {
|
||||
m_entryIds += query.value(QStringLiteral("id")).toString();
|
||||
m_read += query.value(QStringLiteral("read")).toBool();
|
||||
m_new += query.value(QStringLiteral("new")).toBool();
|
||||
m_titles += query.value(QStringLiteral("title")).toString();
|
||||
m_contents += query.value(QStringLiteral("content")).toString();
|
||||
m_feedNames += DataManager::instance().getFeed(query.value(QStringLiteral("feed")).toString())->name();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,41 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QModelIndex>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QVector>
|
||||
|
||||
class EpisodeModel : public QAbstractListModel
|
||||
#include "models/abstractepisodemodel.h"
|
||||
|
||||
class EpisodeModel : public AbstractEpisodeModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
EntryRole = Qt::UserRole,
|
||||
IdRole,
|
||||
ReadRole,
|
||||
NewRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
static EpisodeModel &instance()
|
||||
{
|
||||
static EpisodeModel _instance;
|
||||
return _instance;
|
||||
}
|
||||
explicit EpisodeModel(QObject *parent = nullptr);
|
||||
QVariant data(const QModelIndex &index, int role = Qt::UserRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void updateInternalState();
|
||||
void updateInternalState() override;
|
||||
|
||||
private:
|
||||
QStringList m_entryIds;
|
||||
QVector<bool> m_read;
|
||||
QVector<bool> m_new;
|
||||
QStringList m_titles;
|
||||
QStringList m_contents;
|
||||
QStringList m_feedNames;
|
||||
};
|
||||
|
|
|
@ -1,104 +1,14 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#include "models/episodeproxymodel.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
|
||||
EpisodeProxyModel::EpisodeProxyModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
: AbstractEpisodeProxyModel(parent)
|
||||
{
|
||||
m_currentFilter = NoFilter;
|
||||
m_episodeModel = new EpisodeModel(this);
|
||||
m_episodeModel = &EpisodeModel::instance();
|
||||
setSourceModel(m_episodeModel);
|
||||
}
|
||||
|
||||
bool EpisodeProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
|
||||
{
|
||||
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
|
||||
switch (m_currentFilter) {
|
||||
case NoFilter:
|
||||
return true;
|
||||
case ReadFilter:
|
||||
return sourceModel()->data(index, EpisodeModel::Roles::ReadRole).value<bool>();
|
||||
case NotReadFilter:
|
||||
return !sourceModel()->data(index, EpisodeModel::Roles::ReadRole).value<bool>();
|
||||
case NewFilter:
|
||||
return sourceModel()->data(index, EpisodeModel::Roles::NewRole).value<bool>();
|
||||
case NotNewFilter:
|
||||
return !sourceModel()->data(index, EpisodeModel::Roles::NewRole).value<bool>();
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeProxyModel::FilterType EpisodeProxyModel::filterType() const
|
||||
{
|
||||
return m_currentFilter;
|
||||
}
|
||||
|
||||
void EpisodeProxyModel::setFilterType(FilterType type)
|
||||
{
|
||||
if (type != m_currentFilter) {
|
||||
disconnect(&DataManager::instance(), &DataManager::bulkReadStatusActionFinished, this, nullptr);
|
||||
disconnect(&DataManager::instance(), &DataManager::bulkNewStatusActionFinished, this, nullptr);
|
||||
|
||||
beginResetModel();
|
||||
m_currentFilter = type;
|
||||
m_episodeModel->updateInternalState();
|
||||
endResetModel();
|
||||
|
||||
// connect to signals which indicate that read/new statuses have been updated
|
||||
if (type == ReadFilter || type == NotReadFilter) {
|
||||
connect(&DataManager::instance(), &DataManager::bulkReadStatusActionFinished, this, [this]() {
|
||||
beginResetModel();
|
||||
m_episodeModel->updateInternalState();
|
||||
endResetModel();
|
||||
});
|
||||
} else if (type == NewFilter || type == NotNewFilter) {
|
||||
connect(&DataManager::instance(), &DataManager::bulkNewStatusActionFinished, this, [this]() {
|
||||
beginResetModel();
|
||||
m_episodeModel->updateInternalState();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
Q_EMIT filterTypeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString EpisodeProxyModel::filterName() const
|
||||
{
|
||||
return getFilterName(m_currentFilter);
|
||||
}
|
||||
|
||||
QString EpisodeProxyModel::getFilterName(FilterType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case NoFilter:
|
||||
return i18n("No Filter");
|
||||
case ReadFilter:
|
||||
return i18n("Played Episodes");
|
||||
case NotReadFilter:
|
||||
return i18n("Unplayed Episodes");
|
||||
case NewFilter:
|
||||
return i18n("Episodes marked as \"New\"");
|
||||
case NotNewFilter:
|
||||
return i18n("Episodes not marked as \"New\"");
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection EpisodeProxyModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
}
|
||||
|
|
|
@ -1,51 +1,23 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QItemSelection>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QObject>
|
||||
|
||||
#include "models/abstractepisodeproxymodel.h"
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
class Entry;
|
||||
|
||||
class EpisodeProxyModel : public QSortFilterProxyModel
|
||||
class EpisodeProxyModel : public AbstractEpisodeProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum FilterType {
|
||||
NoFilter,
|
||||
ReadFilter,
|
||||
NotReadFilter,
|
||||
NewFilter,
|
||||
NotNewFilter,
|
||||
};
|
||||
Q_ENUM(FilterType)
|
||||
|
||||
Q_PROPERTY(FilterType filterType READ filterType WRITE setFilterType NOTIFY filterTypeChanged)
|
||||
Q_PROPERTY(QString filterName READ filterName NOTIFY filterTypeChanged)
|
||||
|
||||
explicit EpisodeProxyModel(QObject *parent = nullptr);
|
||||
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
FilterType filterType() const;
|
||||
QString filterName() const;
|
||||
void setFilterType(FilterType type);
|
||||
|
||||
Q_INVOKABLE QString getFilterName(FilterType type) const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTypeChanged();
|
||||
|
||||
private:
|
||||
EpisodeModel *m_episodeModel;
|
||||
FilterType m_currentFilter;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
@ -7,17 +7,17 @@
|
|||
#include "models/queuemodel.h"
|
||||
#include "models/queuemodellogging.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QThread>
|
||||
|
||||
#include <KFormat>
|
||||
|
||||
#include "audiomanager.h"
|
||||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
#include "models/episodemodel.h"
|
||||
#include "settingsmanager.h"
|
||||
|
||||
QueueModel::QueueModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
: AbstractEpisodeModel(parent)
|
||||
{
|
||||
connect(&DataManager::instance(), &DataManager::queueEntryMoved, this, [this](int from, int to_orig) {
|
||||
int to = (from < to_orig) ? to_orig + 1 : to_orig;
|
||||
|
@ -50,25 +50,15 @@ QueueModel::QueueModel(QObject *parent)
|
|||
QVariant QueueModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case EpisodeModel::Roles::EntryRole:
|
||||
case AbstractEpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getQueueEntry(index.row()));
|
||||
case EpisodeModel::Roles::IdRole:
|
||||
case AbstractEpisodeModel::Roles::IdRole:
|
||||
return QVariant::fromValue(DataManager::instance().queue()[index.row()]);
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> QueueModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{EpisodeModel::Roles::EntryRole, "entry"},
|
||||
{EpisodeModel::Roles::IdRole, "id"},
|
||||
{EpisodeModel::Roles::ReadRole, "read"},
|
||||
{EpisodeModel::Roles::NewRole, "new"},
|
||||
};
|
||||
}
|
||||
|
||||
int QueueModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
|
@ -80,7 +70,7 @@ int QueueModel::timeLeft() const
|
|||
{
|
||||
int result = 0;
|
||||
QStringList queue = DataManager::instance().queue();
|
||||
for (QString item : queue) {
|
||||
for (const QString &item : queue) {
|
||||
Entry *entry = DataManager::instance().getEntry(item);
|
||||
if (entry->enclosure()) {
|
||||
result += entry->enclosure()->duration() * 1000 - entry->enclosure()->playPosition();
|
||||
|
@ -101,6 +91,11 @@ QString QueueModel::formattedTimeLeft() const
|
|||
return format.formatDuration(timeLeft() / rate);
|
||||
}
|
||||
|
||||
void QueueModel::updateInternalState()
|
||||
{
|
||||
// nothing to do; DataManager already has the updated data.
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection QueueModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QItemSelection>
|
||||
#include <QModelIndex>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include <KFormat>
|
||||
#include "models/abstractepisodemodel.h"
|
||||
|
||||
#include "audiomanager.h"
|
||||
|
||||
class QueueModel : public QAbstractListModel
|
||||
class QueueModel : public AbstractEpisodeModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
|
@ -29,16 +28,18 @@ public:
|
|||
static QueueModel _instance;
|
||||
return _instance;
|
||||
}
|
||||
explicit QueueModel(QObject * = nullptr);
|
||||
//~QueueModel() override;
|
||||
explicit QueueModel(QObject *parent = nullptr);
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
int timeLeft() const;
|
||||
QString formattedTimeLeft() const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
public Q_SLOTS:
|
||||
void updateInternalState() override;
|
||||
|
||||
Q_SIGNALS:
|
||||
void timeLeftChanged();
|
||||
};
|
||||
|
|
|
@ -35,7 +35,7 @@ FocusScope {
|
|||
var model = pageStack.get(0).queueList.model;
|
||||
for (var i = 0; i < model.rowCount(); i++) {
|
||||
var index = model.index(i, 0);
|
||||
if (AudioManager.entry == model.data(index, EpisodeModel.EntryRole)) {
|
||||
if (AudioManager.entry == model.data(index, AbstractEpisodeModel.EntryRole)) {
|
||||
pageStack.get(0).queueList.currentIndex = i;
|
||||
pageStack.get(0).queueList.selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
|
||||
|
@ -117,7 +117,7 @@ FocusScope {
|
|||
property string blurredImage: AudioManager.entry ? AudioManager.entry.cachedImage : "no-image"
|
||||
property string title: AudioManager.entry ? AudioManager.entry.title : i18n("No Track Title")
|
||||
property string feed: AudioManager.entry ? AudioManager.entry.feed.name : i18n("No Track Loaded")
|
||||
property string authors: AudioManager.entry ? (AudioManager.entry.feed.authors.length !== 0 ? AudioManager.entry.feed.authors[0].name : undefined) : undefined
|
||||
property string authors: AudioManager.entry ? (AudioManager.entry.feed.authors.length !== 0 ? AudioManager.entry.feed.authors[0].name : "") : ""
|
||||
|
||||
implicitHeight: headerBar.handlePosition
|
||||
implicitWidth: parent.width
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
|
||||
* SPDX-FileCopyrightText: 2021-2022 Bart De Vries <bart@mogwai.be>
|
||||
* SPDX-FileCopyrightText: 2021-2023 Bart De Vries <bart@mogwai.be>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
@ -8,7 +8,6 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14 as Controls
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtGraphicalEffects 1.15
|
||||
import org.kde.kirigami 2.19 as Kirigami
|
||||
|
||||
import org.kde.kasts 1.0
|
||||
|
@ -17,6 +16,8 @@ Kirigami.ScrollablePage {
|
|||
id: episodeListPage
|
||||
title: i18n("Episode List")
|
||||
|
||||
property alias episodeList: episodeList
|
||||
|
||||
LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
|
@ -28,29 +29,56 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
icon.name: "download"
|
||||
text: i18n("Downloads")
|
||||
onTriggered: {
|
||||
pushPage("DownloadListPage")
|
||||
Keys.onPressed: {
|
||||
if (event.matches(StandardKey.Find)) {
|
||||
searchActionButton.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
actions.left: Kirigami.Action {
|
||||
icon.name: "view-filter"
|
||||
text: i18n("Filter")
|
||||
onTriggered: filterTypeOverlay.open();
|
||||
}
|
||||
actions {
|
||||
main: Kirigami.Action {
|
||||
icon.name: "download"
|
||||
text: i18n("Downloads")
|
||||
onTriggered: {
|
||||
pushPage("DownloadListPage")
|
||||
}
|
||||
}
|
||||
|
||||
actions.right: Kirigami.Action {
|
||||
icon.name: "view-refresh"
|
||||
text: i18n("Refresh All Podcasts")
|
||||
onTriggered: refreshing = true
|
||||
visible: episodeProxyModel.filterType == EpisodeProxyModel.NoFilter
|
||||
left: Kirigami.Action {
|
||||
id: searchActionButton
|
||||
icon.name: "search"
|
||||
text: i18nc("@action:intoolbar", "Search and Filter")
|
||||
checkable: true
|
||||
onToggled: {
|
||||
if (!checked) {
|
||||
episodeProxyModel.filterType = AbstractEpisodeProxyModel.NoFilter;
|
||||
episodeProxyModel.searchFilter = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
right: Kirigami.Action {
|
||||
icon.name: "view-refresh"
|
||||
text: i18n("Refresh All Podcasts")
|
||||
onTriggered: refreshing = true
|
||||
visible: episodeProxyModel.filterType == AbstractEpisodeProxyModel.NoFilter
|
||||
}
|
||||
}
|
||||
|
||||
contextualActions: episodeList.defaultActionList
|
||||
|
||||
header: Loader {
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
active: searchActionButton.checked
|
||||
visible: active
|
||||
sourceComponent: SearchFilterBar {
|
||||
proxyModel: episodeProxyModel
|
||||
parentKey: searchActionButton
|
||||
}
|
||||
}
|
||||
|
||||
GenericEntryListView {
|
||||
id: episodeList
|
||||
anchors.fill: parent
|
||||
|
@ -65,8 +93,6 @@ Kirigami.ScrollablePage {
|
|||
text: i18n("No Episodes Available")
|
||||
}
|
||||
|
||||
|
||||
|
||||
model: EpisodeProxyModel {
|
||||
id: episodeProxyModel
|
||||
}
|
||||
|
@ -78,71 +104,8 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
margins: Kirigami.Units.largeSpacing
|
||||
bottomMargin: Kirigami.Units.largeSpacing + ( errorNotification.visible ? errorNotification.height + Kirigami.Units.largeSpacing : 0 ) + ( updateNotification.visible ? updateNotification.height + Kirigami.Units.largeSpacing : 0 ) + ( updateSyncNotification.visible ? updateSyncNotification.height + Kirigami.Units.largeSpacing : 0 )
|
||||
}
|
||||
type: Kirigami.MessageType.Information
|
||||
visible: episodeProxyModel.filterType != EpisodeProxyModel.NoFilter
|
||||
text: textMetrics.text
|
||||
width: Math.min(textMetrics.width + 2 * Kirigami.Units.largeSpacing + 10 * Kirigami.Units.gridUnit, parent.width - anchors.leftMargin - anchors.rightMargin)
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: resetButton
|
||||
icon.name: "edit-delete-remove"
|
||||
text: i18n("Reset")
|
||||
onTriggered: {
|
||||
episodeProxyModel.filterType = EpisodeProxyModel.NoFilter;
|
||||
}
|
||||
}
|
||||
]
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
text: i18n("Filter Active: ") + episodeProxyModel.filterName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: filterTypeOverlay
|
||||
|
||||
title: i18n("Select Filter")
|
||||
preferredWidth: Kirigami.Units.gridUnit * 16
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: ListModel {
|
||||
id: filterModel
|
||||
// have to use script because i18n doesn't work within ListElement
|
||||
Component.onCompleted: {
|
||||
var filterList = [EpisodeProxyModel.NoFilter,
|
||||
EpisodeProxyModel.ReadFilter,
|
||||
EpisodeProxyModel.NotReadFilter,
|
||||
EpisodeProxyModel.NewFilter,
|
||||
EpisodeProxyModel.NotNewFilter]
|
||||
for (var i in filterList) {
|
||||
filterModel.append({"name": episodeProxyModel.getFilterName(filterList[i]),
|
||||
"filterType": filterList[i]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Kirigami.BasicListItem {
|
||||
id: swipeDelegate
|
||||
Layout.fillWidth: true
|
||||
highlighted: filterType === episodeProxyModel.filterType
|
||||
text: name
|
||||
onClicked: {
|
||||
episodeProxyModel.filterType = filterType;
|
||||
filterTypeOverlay.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
FilterInlineMessage {
|
||||
proxyModel: episodeProxyModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,12 @@ Kirigami.ScrollablePage {
|
|||
|
||||
title: i18n("Podcast Details")
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.matches(StandardKey.Find)) {
|
||||
searchActionButton.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
supportsRefreshing: true
|
||||
|
||||
onRefreshingChanged: {
|
||||
|
@ -59,11 +65,31 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
|
||||
// add the default actions through onCompleted to add them to the ones
|
||||
// defined above
|
||||
Component.onCompleted: {
|
||||
for (var i in entryList.defaultActionList) {
|
||||
contextualActions.push(entryList.defaultActionList[i]);
|
||||
actions.main: Kirigami.Action {
|
||||
id: searchActionButton
|
||||
icon.name: "search"
|
||||
text: i18nc("@action:intoolbar", "Search and Filter")
|
||||
checkable: true
|
||||
enabled: page.feed.entries ? true : false
|
||||
visible: enabled
|
||||
onToggled: {
|
||||
if (!checked && page.feed.entries) {
|
||||
page.feed.entries.filterType = AbstractEpisodeProxyModel.NoFilter;
|
||||
page.feed.entries.searchFilter = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: Loader {
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
active: searchActionButton.checked
|
||||
visible: active
|
||||
|
||||
sourceComponent: SearchFilterBar {
|
||||
proxyModel: page.feed.entries ? page.feed.entries : emptyListModel
|
||||
parentKey: searchActionButton
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,17 +112,12 @@ Kirigami.ScrollablePage {
|
|||
model: page.feed.entries ? page.feed.entries : emptyListModel
|
||||
delegate: entryListDelegate
|
||||
|
||||
// OverlayHeader looks nicer, but seems completely broken when flicking the list
|
||||
//headerPositioning: ListView.OverlayHeader
|
||||
|
||||
header: ColumnLayout {
|
||||
id: headerColumn
|
||||
height: (isSubscribed && entryList.count > 0) ? implicitHeight : entryList.height
|
||||
width: entryList.width
|
||||
spacing: 0
|
||||
|
||||
property real headerOverlayProgress: Math.min(1, Math.abs(entryList.contentY) / headerColumn.height)
|
||||
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
|
||||
|
@ -119,15 +140,18 @@ Kirigami.ScrollablePage {
|
|||
topPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Rectangle {
|
||||
color: Kirigami.Theme.alternateBackgroundColor
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Header
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
}
|
||||
|
||||
contentItem: Kirigami.ActionToolBar {
|
||||
id: feedToolBar
|
||||
alignment: Qt.AlignLeft
|
||||
background: Item {}
|
||||
|
||||
// HACK: ActionToolBar loads buttons dynamically, and so the height calculation
|
||||
// changes the position
|
||||
// HACK: ActionToolBar loads buttons dynamically, and so the
|
||||
// height calculation changes the position
|
||||
onHeightChanged: entryList.contentY = entryList.originY
|
||||
|
||||
actions: [
|
||||
|
@ -161,6 +185,16 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
]
|
||||
|
||||
// add the default actions through onCompleted to add them
|
||||
// to the ones defined above
|
||||
Component.onCompleted: {
|
||||
if (isSubscribed) {
|
||||
for (var i in entryList.defaultActionList) {
|
||||
feedToolBar.actions.push(entryList.defaultActionList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,5 +367,9 @@ Kirigami.ScrollablePage {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
FilterInlineMessage {
|
||||
proxyModel: page.feed.entries ? page.feed.entries : emptyListModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 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 org.kde.kirigami 2.19 as Kirigami
|
||||
|
||||
import org.kde.kasts 1.0
|
||||
|
||||
|
||||
Kirigami.InlineMessage {
|
||||
required property var proxyModel
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
margins: Kirigami.Units.largeSpacing
|
||||
bottomMargin: Kirigami.Units.largeSpacing + ( errorNotification.visible ? errorNotification.height + Kirigami.Units.largeSpacing : 0 ) + ( updateNotification.visible ? updateNotification.height + Kirigami.Units.largeSpacing : 0 ) + ( updateSyncNotification.visible ? updateSyncNotification.height + Kirigami.Units.largeSpacing : 0 )
|
||||
}
|
||||
type: Kirigami.MessageType.Information
|
||||
visible: proxyModel.filterType != AbstractEpisodeProxyModel.NoFilter
|
||||
text: textMetrics.text
|
||||
width: Math.min(textMetrics.width + 2 * Kirigami.Units.largeSpacing + 10 * Kirigami.Units.gridUnit, parent.width - anchors.leftMargin - anchors.rightMargin)
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
id: resetButton
|
||||
icon.name: "edit-delete-remove"
|
||||
text: i18nc("@action:button Reset filters active on ListView", "Reset")
|
||||
onTriggered: {
|
||||
proxyModel.filterType = AbstractEpisodeProxyModel.NoFilter;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
text: i18nc("@info:status Name of the filter which is active on the ListView", "Filter Active: %1", proxyModel.filterName)
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ ListView {
|
|||
|
||||
onSelectionForContextMenuChanged: {
|
||||
if (selectionForContextMenu.length === 1) {
|
||||
singleSelectedEntry = selectionForContextMenu[0].model.data(selectionForContextMenu[0], EpisodeModel.EntryRole);
|
||||
singleSelectedEntry = selectionForContextMenu[0].model.data(selectionForContextMenu[0], AbstractEpisodeModel.EntryRole);
|
||||
} else {
|
||||
singleSelectedEntry = undefined;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 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.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtQml.Models 2.15
|
||||
|
||||
import org.kde.kirigami 2.12 as Kirigami
|
||||
import org.kde.kirigamiaddons.labs.components 1.0 as Addons
|
||||
|
||||
import org.kde.kasts 1.0
|
||||
|
||||
Addons.SearchPopupField {
|
||||
id: globalSearchField
|
||||
spaceAvailableLeft: false
|
||||
spaceAvailableRight: true
|
||||
|
||||
autoAccept: false
|
||||
|
||||
popup.width: Math.min(Kirigami.Units.gridUnit * 20, kastsMainWindow.width - Kirigami.Units.gridUnit * 2)
|
||||
|
||||
property string searchFilter: ""
|
||||
|
||||
onAccepted: {
|
||||
globalSearchField.searchFilter = globalSearchField.text
|
||||
}
|
||||
|
||||
popup.onClosed: {
|
||||
globalSearchField.text = ""
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (globalSearchField.text === "") {
|
||||
globalSearchField.searchFilter = "";
|
||||
}
|
||||
}
|
||||
|
||||
function openEntry(entry) {
|
||||
pushPage("EpisodeListPage");
|
||||
pageStack.push("qrc:/EntryPage.qml", {"entry": entry});
|
||||
|
||||
/* let's not select the current item for now, since it can take a long
|
||||
* time and will not automatically scroll to that item either
|
||||
var episodeModel = pageStack.get(0).episodeList.model
|
||||
for (var i = 0; i < episodeModel.rowCount(); i++) {
|
||||
var index = episodeModel.index(i, 0);
|
||||
if (entry == episodeModel.data(index, AbstractEpisodeModel.EntryRole)) {
|
||||
pageStack.get(0).episodeList.currentIndex = i;
|
||||
pageStack.get(0).episodeList.selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// rightActions are defined from right-to-left
|
||||
// if we want to insert the settings action as the rightmost, then it
|
||||
// must be defined as first action, which means that we need to save the
|
||||
// default clear action and push that as a second action
|
||||
var origAction = searchField.rightActions[0];
|
||||
searchField.rightActions[0] = searchSettingsButton;
|
||||
searchField.rightActions.push(origAction);
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: searchListView
|
||||
reuseItems: true
|
||||
|
||||
EpisodeProxyModel {
|
||||
id: proxyModel
|
||||
searchFilter: globalSearchField.searchFilter
|
||||
}
|
||||
|
||||
model: globalSearchField.searchFilter === "" ? null : proxyModel
|
||||
|
||||
delegate: Component {
|
||||
Kirigami.BasicListItem {
|
||||
separatorVisible: true
|
||||
icon: model.entry.cachedImage
|
||||
label: model.entry.title
|
||||
subtitle: model.entry.feed.name
|
||||
onClicked: {
|
||||
globalSearchField.openEntry(model.entry);
|
||||
globalSearchField.popup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Kirigami.PlaceholderMessage {
|
||||
id: loadingPlaceholder
|
||||
anchors.fill: parent
|
||||
visible: searchListView.count === 0
|
||||
|
||||
text: i18nc("@info Placeholder text in search box", "No Search Results")
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
id: searchSettingsButton
|
||||
visible: globalSearchField.popup.visible
|
||||
icon.name: "settings-configure"
|
||||
text: i18nc("@action:intoolbar", "Advanced Search Options")
|
||||
|
||||
onTriggered: {
|
||||
if (searchSettingsMenu.visible) {
|
||||
searchSettingsMenu.dismiss();
|
||||
} else {
|
||||
searchSettingsMenu.popup(searchSettingsButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: searchSettingsModel
|
||||
|
||||
function reload() {
|
||||
clear();
|
||||
var searchList = [AbstractEpisodeProxyModel.TitleFlag,
|
||||
AbstractEpisodeProxyModel.ContentFlag,
|
||||
AbstractEpisodeProxyModel.FeedNameFlag]
|
||||
for (var i in searchList) {
|
||||
searchSettingsModel.append({"name": proxyModel.getSearchFlagName(searchList[i]),
|
||||
"searchFlag": searchList[i],
|
||||
"checked": proxyModel.searchFlags & searchList[i]});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Menu {
|
||||
id: searchSettingsMenu
|
||||
|
||||
title: i18nc("@title:menu", "Search Preferences")
|
||||
|
||||
Controls.Label {
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
text: i18nc("@title:group Group of fields in which can be searched", "Search in:")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: searchSettingsModel
|
||||
|
||||
Controls.CheckBox {
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
text: model.name
|
||||
checked: model.checked
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
proxyModel.searchFlags = proxyModel.searchFlags | model.searchFlag;
|
||||
} else {
|
||||
proxyModel.searchFlags = proxyModel.searchFlags & ~model.searchFlag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
searchSettingsModel.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 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
|
||||
|
||||
Controls.Control {
|
||||
id: searchFilterBar
|
||||
|
||||
required property var proxyModel
|
||||
required property var parentKey
|
||||
|
||||
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.largeSpacing
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
background: Rectangle {
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Header
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
|
||||
Kirigami.Separator {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
Kirigami.SearchField {
|
||||
Layout.fillWidth: true
|
||||
id: searchField
|
||||
placeholderText: i18nc("@label:textbox Placeholder text for episode search field", "Search Episodes")
|
||||
focus: true
|
||||
autoAccept: false
|
||||
onAccepted: {
|
||||
proxyModel.searchFilter = searchField.text;
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
id: searchSettingsButton
|
||||
icon.name: "settings-configure"
|
||||
text: i18nc("@action:intoolbar", "Advanced Search Options")
|
||||
|
||||
onTriggered: {
|
||||
if (searchSettingsMenu.visible) {
|
||||
searchSettingsMenu.dismiss();
|
||||
} else {
|
||||
searchSettingsMenu.popup(searchSettingsButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
// rightActions are defined from right-to-left
|
||||
// if we want to insert the settings action as the rightmost, then it
|
||||
// must be defined as first action, which means that we need to save the
|
||||
// default clear action and push that as a second action
|
||||
var origAction = searchField.rightActions[0];
|
||||
searchField.rightActions[0] = searchSettingsButton;
|
||||
searchField.rightActions.push(origAction);
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
proxyModel.filterType = AbstractEpisodeProxyModel.NoFilter;
|
||||
proxyModel.searchFilter = "";
|
||||
parentKey.checked = false;
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
accepted();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Controls.ToolButton {
|
||||
id: filterButton
|
||||
icon.name: "view-filter"
|
||||
text: i18nc("@action:intoolbar Button to open menu to filter episodes based on their status (played, new, etc.)", "Filter")
|
||||
display: wideScreen ? Controls.AbstractButton.TextBesideIcon : Controls.AbstractButton.IconOnly
|
||||
|
||||
Controls.ToolTip.visible: hovered
|
||||
Controls.ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
|
||||
Controls.ToolTip.text: i18nc("@info:tooltip", "Filter Episodes by Status")
|
||||
|
||||
onPressed: {
|
||||
if (filterMenu.visible) {
|
||||
filterMenu.dismiss();
|
||||
} else {
|
||||
filterMenu.popup(filterButton, filterButton.x, filterButton.y + filterButton.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filterModel
|
||||
|
||||
// have to use script because i18n doesn't work within ListElement
|
||||
Component.onCompleted: {
|
||||
var filterList = [AbstractEpisodeProxyModel.NoFilter,
|
||||
AbstractEpisodeProxyModel.ReadFilter,
|
||||
AbstractEpisodeProxyModel.NotReadFilter,
|
||||
AbstractEpisodeProxyModel.NewFilter,
|
||||
AbstractEpisodeProxyModel.NotNewFilter]
|
||||
for (var i in filterList) {
|
||||
filterModel.append({"name": proxyModel.getFilterName(filterList[i]),
|
||||
"filterType": filterList[i]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Menu {
|
||||
id: filterMenu
|
||||
|
||||
title: i18nc("@title:menu", "Select Filter")
|
||||
|
||||
Controls.ButtonGroup { id: filterGroup }
|
||||
|
||||
Repeater {
|
||||
model: filterModel
|
||||
|
||||
Controls.RadioButton {
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
text: model.name
|
||||
checked: model.filterType === proxyModel.filterType
|
||||
Controls.ButtonGroup.group: filterGroup
|
||||
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
proxyModel.filterType = model.filterType;
|
||||
}
|
||||
filterMenu.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: searchSettingsModel
|
||||
|
||||
function reload() {
|
||||
clear();
|
||||
var searchList = [AbstractEpisodeProxyModel.TitleFlag,
|
||||
AbstractEpisodeProxyModel.ContentFlag,
|
||||
AbstractEpisodeProxyModel.FeedNameFlag]
|
||||
for (var i in searchList) {
|
||||
searchSettingsModel.append({"name": proxyModel.getSearchFlagName(searchList[i]),
|
||||
"searchFlag": searchList[i],
|
||||
"checked": proxyModel.searchFlags & searchList[i]});
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
|
||||
Controls.Menu {
|
||||
id: searchSettingsMenu
|
||||
|
||||
title: i18nc("@title:menu", "Search Preferences")
|
||||
|
||||
Controls.Label {
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
text: i18nc("@title:group Group of fields in which can be searched", "Search in:")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: searchSettingsModel
|
||||
|
||||
Controls.CheckBox {
|
||||
padding: Kirigami.Units.smallSpacing
|
||||
text: model.name
|
||||
checked: model.checked
|
||||
onToggled: {
|
||||
if (checked) {
|
||||
proxyModel.searchFlags = proxyModel.searchFlags | model.searchFlag;
|
||||
} else {
|
||||
proxyModel.searchFlags = proxyModel.searchFlags & ~model.searchFlag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
searchSettingsModel.reload();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -131,15 +131,13 @@ Kirigami.ApplicationWindow {
|
|||
sourceComponent: Kirigami.OverlayDrawer {
|
||||
id: drawer
|
||||
modal: false
|
||||
|
||||
readonly property real listViewThreshold: Kirigami.Settings.isMobile ? Kirigami.Units.gridUnit * 22 : Kirigami.Units.gridUnit * 20
|
||||
closePolicy: Controls.Popup.NoAutoClose
|
||||
|
||||
readonly property real pinnedWidth: Kirigami.Units.gridUnit * 3
|
||||
readonly property real widescreenSmallWidth: Kirigami.Units.gridUnit * 6
|
||||
readonly property real widescreenBigWidth: Kirigami.Units.gridUnit * 10
|
||||
readonly property int buttonDisplayMode: kastsMainWindow.isWidescreen ? (drawer.height < listViewThreshold ? Kirigami.NavigationTabButton.TextBesideIcon : Kirigami.NavigationTabButton.TextUnderIcon) : Kirigami.NavigationTabButton.IconOnly
|
||||
readonly property int buttonDisplayMode: kastsMainWindow.isWidescreen ? Kirigami.NavigationTabButton.TextBesideIcon : Kirigami.NavigationTabButton.IconOnly
|
||||
|
||||
width: kastsMainWindow.isWidescreen ? (drawer.height < listViewThreshold ? widescreenBigWidth : widescreenSmallWidth) : pinnedWidth
|
||||
width: kastsMainWindow.isWidescreen ? widescreenBigWidth : pinnedWidth
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
Kirigami.Theme.inherit: false
|
||||
|
@ -152,11 +150,16 @@ Kirigami.ApplicationWindow {
|
|||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
Loader {
|
||||
active: Kirigami.Settings.isMobile
|
||||
visible: active
|
||||
Controls.ToolBar {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Kirigami.AbstractApplicationHeader { }
|
||||
Layout.preferredHeight: pageStack.globalToolBar.preferredHeight
|
||||
|
||||
leftPadding: Kirigami.Units.smallSpacing
|
||||
rightPadding: Kirigami.Units.smallSpacing
|
||||
topPadding: Kirigami.Units.smallSpacing
|
||||
bottomPadding: Kirigami.Units.smallSpacing
|
||||
|
||||
contentItem: GlobalSearchField {}
|
||||
}
|
||||
|
||||
Controls.ScrollView {
|
||||
|
|
|
@ -41,6 +41,9 @@
|
|||
<file alias="ErrorListPage.qml">qml/Settings/ErrorListPage.qml</file>
|
||||
<file alias="SleepTimerDialog.qml">qml/SleepTimerDialog.qml</file>
|
||||
<file alias="FullScreenImage.qml">qml/FullScreenImage.qml</file>
|
||||
<file alias="GlobalSearchField.qml">qml/GlobalSearchField.qml</file>
|
||||
<file alias="SearchFilterBar.qml">qml/SearchFilterBar.qml</file>
|
||||
<file alias="FilterInlineMessage.qml">qml/FilterInlineMessage.qml</file>
|
||||
<file>qtquickcontrols2.conf</file>
|
||||
<file alias="logo.svg">../kasts.svg</file>
|
||||
<file alias="kasts-tray.svg">../icons/kasts-tray.svg</file>
|
||||
|
|
Loading…
Reference in New Issue