Enable multi-selection and context menus on all lists of entries
This commit adds keyboard navigation to entry lists. Selection of items can be done through keyboard (shift+up/down), mouse (left, left+shift, left+ctrl) or touch (long press). When items are selected, contextual actions will show up on the page (useful for touch screens), or, alternatively, a context menu with the same actions can be opened through right mouse click (useful for desktop). If a single entry is selected, then only the relevant actions will be shown (e.g. only "Mark as Played" if the entry has not been played yet). Additionally, (database) transactions for the actions have been optimized. This was necessary to make sure that actions on large selections of entries finish within an acceptable time. E.g. actions on a list of 1000 items should finish within a few seconds (on all but underpowered hardware). BUG: 441764
This commit is contained in:
parent
6121f6622f
commit
4b2da3533c
@ -149,6 +149,8 @@ if(ANDROID)
|
||||
document-open-folder
|
||||
edit-delete-remove
|
||||
edit-clear-all
|
||||
edit-select-all
|
||||
edit-select-none
|
||||
download
|
||||
media-playlist-append
|
||||
media-seek-backward
|
||||
|
@ -76,7 +76,7 @@ DataManager::DataManager()
|
||||
Database::instance().execute(query);
|
||||
while (query.next()) {
|
||||
QString id = query.value(QStringLiteral("id")).toString();
|
||||
addToQueue(feedurl, id);
|
||||
getEntry(id)->setQueueStatusInternal(true);
|
||||
if (SettingsManager::self()->autoDownload()) {
|
||||
if (getEntry(id) && getEntry(id)->hasEnclosure() && getEntry(id)->enclosure()) {
|
||||
qCDebug(kastsDataManager) << "Start downloading" << getEntry(id)->title();
|
||||
@ -152,6 +152,11 @@ int DataManager::feedCount() const
|
||||
return m_feedmap.count();
|
||||
}
|
||||
|
||||
QStringList DataManager::getIdList(const Feed *feed) const
|
||||
{
|
||||
return m_entrymap[feed->url()];
|
||||
}
|
||||
|
||||
int DataManager::entryCount(const int feed_index) const
|
||||
{
|
||||
return m_entrymap[m_feedmap[feed_index]].count();
|
||||
@ -199,14 +204,16 @@ void DataManager::removeFeed(const int index)
|
||||
// Delete the object instances and mappings
|
||||
// First delete entries in Queue
|
||||
qCDebug(kastsDataManager) << "delete queueentries of" << feedurl;
|
||||
QStringList removeFromQueueList;
|
||||
for (auto &id : m_queuemap) {
|
||||
if (getEntry(id)->feed()->url() == feedurl) {
|
||||
if (AudioManager::instance().entry() == getEntry(id)) {
|
||||
AudioManager::instance().next();
|
||||
}
|
||||
removeQueueItem(id);
|
||||
removeFromQueueList += id;
|
||||
}
|
||||
}
|
||||
bulkQueueStatus(false, removeFromQueueList);
|
||||
|
||||
// Delete entries themselves
|
||||
qCDebug(kastsDataManager) << "delete entries of" << feedurl;
|
||||
@ -326,23 +333,27 @@ QStringList DataManager::queue() const
|
||||
|
||||
bool DataManager::entryInQueue(const Entry *entry)
|
||||
{
|
||||
return entryInQueue(entry->feed()->url(), entry->id());
|
||||
return entryInQueue(entry->id());
|
||||
}
|
||||
|
||||
bool DataManager::entryInQueue(const QString &feedurl, const QString &id) const
|
||||
bool DataManager::entryInQueue(const QString &id) const
|
||||
{
|
||||
Q_UNUSED(feedurl);
|
||||
return m_queuemap.contains(id);
|
||||
}
|
||||
|
||||
void DataManager::addToQueue(const Entry *entry)
|
||||
void DataManager::moveQueueItem(const int from, const int to)
|
||||
{
|
||||
if (entry != nullptr) {
|
||||
return addToQueue(entry->feed()->url(), entry->id());
|
||||
}
|
||||
// First move the items in the internal data structure
|
||||
m_queuemap.move(from, to);
|
||||
|
||||
// Then make sure that the database Queue table reflects these changes
|
||||
updateQueueListnrs();
|
||||
|
||||
// Make sure that the QueueModel is aware of the changes so it can update
|
||||
Q_EMIT queueEntryMoved(from, to);
|
||||
}
|
||||
|
||||
void DataManager::addToQueue(const QString &feedurl, const QString &id)
|
||||
void DataManager::addToQueue(const QString &id)
|
||||
{
|
||||
// If item is already in queue, then stop here
|
||||
if (m_queuemap.contains(id))
|
||||
@ -359,43 +370,28 @@ void DataManager::addToQueue(const QString &feedurl, const QString &id)
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("INSERT INTO Queue VALUES (:index, :feedurl, :id, :playing);"));
|
||||
query.bindValue(QStringLiteral(":index"), index);
|
||||
query.bindValue(QStringLiteral(":feedurl"), feedurl);
|
||||
query.bindValue(QStringLiteral(":feedurl"), getEntry(id)->feed()->url());
|
||||
query.bindValue(QStringLiteral(":id"), id);
|
||||
query.bindValue(QStringLiteral(":playing"), false);
|
||||
Database::instance().execute(query);
|
||||
|
||||
// Set status to unplayed/unread when adding item to the queue
|
||||
if (getEntry(id)) {
|
||||
getEntry(id)->setRead(false);
|
||||
}
|
||||
|
||||
// Make sure that the QueueModel is aware of the changes
|
||||
Q_EMIT queueEntryAdded(index, id);
|
||||
}
|
||||
|
||||
void DataManager::moveQueueItem(const int from, const int to)
|
||||
void DataManager::removeFromQueue(const QString &id)
|
||||
{
|
||||
// First move the items in the internal data structure
|
||||
m_queuemap.move(from, to);
|
||||
if (!entryInQueue(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Then make sure that the database Queue table reflects these changes
|
||||
updateQueueListnrs();
|
||||
|
||||
// Make sure that the QueueModel is aware of the changes so it can update
|
||||
Q_EMIT queueEntryMoved(from, to);
|
||||
}
|
||||
|
||||
void DataManager::removeQueueItem(const int index)
|
||||
{
|
||||
const int index = m_queuemap.indexOf(id);
|
||||
qCDebug(kastsDataManager) << "Queuemap is now:" << m_queuemap;
|
||||
// Unset "new" state
|
||||
getEntry(m_queuemap[index])->setNew(false);
|
||||
qCDebug(kastsDataManager) << "Queue index of item to be removed" << index;
|
||||
|
||||
const QString id = m_queuemap[index];
|
||||
|
||||
// Unload track from AudioManager if it's currently playing
|
||||
// Move to next track if it's currently playing
|
||||
if (AudioManager::instance().entry() == getEntry(id)) {
|
||||
AudioManager::instance().setEntry(nullptr);
|
||||
AudioManager::instance().next();
|
||||
}
|
||||
|
||||
// Remove the item from the internal data structure
|
||||
@ -403,26 +399,14 @@ void DataManager::removeQueueItem(const int index)
|
||||
|
||||
// Then make sure that the database Queue table reflects these changes
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("DELETE FROM Queue WHERE listnr=:listnr;"));
|
||||
query.bindValue(QStringLiteral(":listnr"), index);
|
||||
query.prepare(QStringLiteral("DELETE FROM Queue WHERE id=:id;"));
|
||||
query.bindValue(QStringLiteral(":id"), id);
|
||||
Database::instance().execute(query);
|
||||
// ... and update all other listnrs in Queue table
|
||||
updateQueueListnrs();
|
||||
|
||||
// Make sure that the QueueModel is aware of the change so it can update
|
||||
Q_EMIT queueEntryRemoved(index, id);
|
||||
}
|
||||
|
||||
void DataManager::removeQueueItem(const QString id)
|
||||
{
|
||||
removeQueueItem(m_queuemap.indexOf(id));
|
||||
}
|
||||
|
||||
void DataManager::removeQueueItem(Entry *entry)
|
||||
{
|
||||
removeQueueItem(m_queuemap.indexOf(entry->id()));
|
||||
}
|
||||
|
||||
QString DataManager::lastPlayingEntry()
|
||||
{
|
||||
QSqlQuery query;
|
||||
@ -541,8 +525,8 @@ bool DataManager::feedExists(const QString &url)
|
||||
void DataManager::updateQueueListnrs() const
|
||||
{
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("UPDATE Queue SET listnr=:i WHERE id=:id;"));
|
||||
for (int i = 0; i < m_queuemap.count(); i++) {
|
||||
query.prepare(QStringLiteral("UPDATE Queue SET listnr=:i WHERE id=:id;"));
|
||||
query.bindValue(QStringLiteral(":i"), i);
|
||||
query.bindValue(QStringLiteral(":id"), m_queuemap[i]);
|
||||
Database::instance().execute(query);
|
||||
@ -553,3 +537,109 @@ bool DataManager::isFeedExists(const QString &url)
|
||||
{
|
||||
return m_feeds.contains(url);
|
||||
}
|
||||
|
||||
void DataManager::bulkMarkReadByIndex(bool state, QModelIndexList list)
|
||||
{
|
||||
bulkMarkRead(state, getIdsFromModelIndexList(list));
|
||||
}
|
||||
|
||||
void DataManager::bulkMarkRead(bool state, QStringList list)
|
||||
{
|
||||
Database::instance().execute(QStringLiteral("BEGIN TRANSACTION;"));
|
||||
|
||||
if (state) { // Mark as read
|
||||
// This needs special attention as the DB operations are very intensive.
|
||||
// Reversing the loop is much faster
|
||||
for (int i = list.count() - 1; i >= 0; i--) {
|
||||
getEntry(list[i])->setReadInternal(state);
|
||||
}
|
||||
updateQueueListnrs(); // update queue after modification
|
||||
} else { // Mark as unread
|
||||
for (QString id : list) {
|
||||
getEntry(id)->setReadInternal(state);
|
||||
}
|
||||
}
|
||||
Database::instance().execute(QStringLiteral("COMMIT;"));
|
||||
|
||||
Q_EMIT bulkReadStatusActionFinished();
|
||||
}
|
||||
|
||||
void DataManager::bulkMarkNewByIndex(bool state, QModelIndexList list)
|
||||
{
|
||||
bulkMarkNew(state, getIdsFromModelIndexList(list));
|
||||
}
|
||||
|
||||
void DataManager::bulkMarkNew(bool state, QStringList list)
|
||||
{
|
||||
Database::instance().execute(QStringLiteral("BEGIN TRANSACTION;"));
|
||||
for (QString id : list) {
|
||||
getEntry(id)->setNewInternal(state);
|
||||
}
|
||||
Database::instance().execute(QStringLiteral("COMMIT;"));
|
||||
|
||||
Q_EMIT bulkNewStatusActionFinished();
|
||||
}
|
||||
|
||||
void DataManager::bulkQueueStatusByIndex(bool state, QModelIndexList list)
|
||||
{
|
||||
bulkQueueStatus(state, getIdsFromModelIndexList(list));
|
||||
}
|
||||
|
||||
void DataManager::bulkQueueStatus(bool state, QStringList list)
|
||||
{
|
||||
Database::instance().execute(QStringLiteral("BEGIN TRANSACTION;"));
|
||||
if (state) { // i.e. add to queue
|
||||
for (QString id : list) {
|
||||
getEntry(id)->setQueueStatusInternal(state);
|
||||
}
|
||||
} else { // i.e. remove from queue
|
||||
// This needs special attention as the DB operations are very intensive.
|
||||
// Reversing the loop is much faster.
|
||||
for (int i = list.count() - 1; i >= 0; i--) {
|
||||
qCDebug(kastsDataManager) << "getting entry" << getEntry(list[i])->id();
|
||||
getEntry(list[i])->setQueueStatusInternal(state);
|
||||
}
|
||||
updateQueueListnrs();
|
||||
}
|
||||
Database::instance().execute(QStringLiteral("COMMIT;"));
|
||||
|
||||
Q_EMIT bulkReadStatusActionFinished();
|
||||
Q_EMIT bulkNewStatusActionFinished();
|
||||
}
|
||||
|
||||
void DataManager::bulkDownloadEnclosuresByIndex(QModelIndexList list)
|
||||
{
|
||||
bulkDownloadEnclosures(getIdsFromModelIndexList(list));
|
||||
}
|
||||
|
||||
void DataManager::bulkDownloadEnclosures(QStringList list)
|
||||
{
|
||||
bulkQueueStatus(true, list);
|
||||
for (QString id : list) {
|
||||
getEntry(id)->enclosure()->download();
|
||||
}
|
||||
}
|
||||
|
||||
void DataManager::bulkDeleteEnclosuresByIndex(QModelIndexList list)
|
||||
{
|
||||
bulkDeleteEnclosures(getIdsFromModelIndexList(list));
|
||||
}
|
||||
|
||||
void DataManager::bulkDeleteEnclosures(QStringList list)
|
||||
{
|
||||
Database::instance().execute(QStringLiteral("BEGIN TRANSACTION;"));
|
||||
for (QString id : list) {
|
||||
getEntry(id)->enclosure()->deleteFile();
|
||||
}
|
||||
Database::instance().execute(QStringLiteral("COMMIT;"));
|
||||
}
|
||||
|
||||
QStringList DataManager::getIdsFromModelIndexList(const QModelIndexList &list) const
|
||||
{
|
||||
QStringList ids;
|
||||
for (QModelIndex index : list) {
|
||||
ids += index.data(EpisodeModel::Roles::IdRole).value<QString>();
|
||||
}
|
||||
qCDebug(kastsDataManager) << "Ids of selection:" << ids;
|
||||
return ids;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ public:
|
||||
Entry *getEntry(const Feed *feed, const int entry_index) const;
|
||||
Q_INVOKABLE Entry *getEntry(const QString &id) const;
|
||||
int feedCount() const;
|
||||
QStringList getIdList(const Feed *feed) const;
|
||||
int entryCount(const int feed_index) const;
|
||||
int entryCount(const Feed *feed) const;
|
||||
int unreadEntryCount(const Feed *feed) const;
|
||||
@ -46,14 +47,11 @@ public:
|
||||
Entry *getQueueEntry(int index) const;
|
||||
int queueCount() const;
|
||||
QStringList queue() const;
|
||||
Q_INVOKABLE bool entryInQueue(const Entry *entry);
|
||||
Q_INVOKABLE bool entryInQueue(const QString &feedurl, const QString &id) const;
|
||||
Q_INVOKABLE void addToQueue(const Entry *entry);
|
||||
Q_INVOKABLE void addToQueue(const QString &feedurl, const QString &id);
|
||||
bool entryInQueue(const Entry *entry);
|
||||
bool entryInQueue(const QString &id) const;
|
||||
Q_INVOKABLE void moveQueueItem(const int from, const int to);
|
||||
Q_INVOKABLE void removeQueueItem(const int index);
|
||||
Q_INVOKABLE void removeQueueItem(const QString id);
|
||||
Q_INVOKABLE void removeQueueItem(Entry *entry);
|
||||
void addToQueue(const QString &id);
|
||||
void removeFromQueue(const QString &id);
|
||||
|
||||
Q_INVOKABLE QString lastPlayingEntry();
|
||||
Q_INVOKABLE void setLastPlayingEntry(const QString &id);
|
||||
@ -64,6 +62,18 @@ public:
|
||||
Q_INVOKABLE void exportFeeds(const QString &path);
|
||||
Q_INVOKABLE bool isFeedExists(const QString &url);
|
||||
|
||||
Q_INVOKABLE void bulkMarkRead(bool state, QStringList list);
|
||||
Q_INVOKABLE void bulkMarkNew(bool state, QStringList list);
|
||||
Q_INVOKABLE void bulkQueueStatus(bool state, QStringList list);
|
||||
Q_INVOKABLE void bulkDownloadEnclosures(QStringList list);
|
||||
Q_INVOKABLE void bulkDeleteEnclosures(QStringList list);
|
||||
|
||||
Q_INVOKABLE void bulkMarkReadByIndex(bool state, QModelIndexList list);
|
||||
Q_INVOKABLE void bulkMarkNewByIndex(bool state, QModelIndexList list);
|
||||
Q_INVOKABLE void bulkQueueStatusByIndex(bool state, QModelIndexList list);
|
||||
Q_INVOKABLE void bulkDownloadEnclosuresByIndex(QModelIndexList list);
|
||||
Q_INVOKABLE void bulkDeleteEnclosuresByIndex(QModelIndexList list);
|
||||
|
||||
Q_SIGNALS:
|
||||
void feedAdded(const QString &url);
|
||||
void feedRemoved(const int &index);
|
||||
@ -77,6 +87,9 @@ Q_SIGNALS:
|
||||
void unreadEntryCountChanged(const QString &url);
|
||||
void newEntryCountChanged(const QString &url);
|
||||
|
||||
void bulkReadStatusActionFinished();
|
||||
void bulkNewStatusActionFinished();
|
||||
|
||||
private:
|
||||
DataManager();
|
||||
void loadFeed(QString feedurl) const;
|
||||
@ -84,6 +97,8 @@ private:
|
||||
bool feedExists(const QString &url);
|
||||
void updateQueueListnrs() const;
|
||||
|
||||
QStringList getIdsFromModelIndexList(const QModelIndexList &list) const;
|
||||
|
||||
mutable QHash<QString, Feed *> m_feeds; // hash of pointers to all feeds in db, key = url (lazy loading)
|
||||
mutable QHash<QString, Entry *> m_entries; // hash of pointers to all entries in db, key = id (lazy loading)
|
||||
|
||||
|
@ -87,6 +87,10 @@ Enclosure::Status Enclosure::dbToStatus(int value)
|
||||
|
||||
void Enclosure::download()
|
||||
{
|
||||
if (m_status == Downloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Fetcher::instance().isMeteredConnection() && !SettingsManager::self()->allowMeteredEpisodeDownloads()) {
|
||||
Q_EMIT downloadError(Error::Type::MeteredNotAllowed,
|
||||
m_entry->feed()->url(),
|
||||
|
140
src/entry.cpp
140
src/entry.cpp
@ -18,7 +18,7 @@
|
||||
#include "settingsmanager.h"
|
||||
|
||||
Entry::Entry(Feed *feed, const QString &id)
|
||||
: QObject(nullptr)
|
||||
: QObject(&DataManager::instance())
|
||||
, m_feed(feed)
|
||||
{
|
||||
connect(&Fetcher::instance(), &Fetcher::downloadFinished, this, [this](QString url) {
|
||||
@ -30,16 +30,7 @@ Entry::Entry(Feed *feed, const QString &id)
|
||||
Q_EMIT cachedImageChanged(cachedImage());
|
||||
}
|
||||
});
|
||||
connect(&DataManager::instance(), &DataManager::queueEntryAdded, this, [this](const int &index, const QString &id) {
|
||||
Q_UNUSED(index)
|
||||
if (id == m_id)
|
||||
Q_EMIT queueStatusChanged(queueStatus());
|
||||
});
|
||||
connect(&DataManager::instance(), &DataManager::queueEntryRemoved, this, [this](const int &index, const QString &id) {
|
||||
Q_UNUSED(index)
|
||||
if (id == m_id)
|
||||
Q_EMIT queueStatusChanged(queueStatus());
|
||||
});
|
||||
|
||||
QSqlQuery entryQuery;
|
||||
entryQuery.prepare(QStringLiteral("SELECT * FROM Entries WHERE feed=:feed AND id=:id;"));
|
||||
entryQuery.bindValue(QStringLiteral(":feed"), m_feed->url());
|
||||
@ -134,50 +125,83 @@ QString Entry::baseUrl() const
|
||||
|
||||
void Entry::setRead(bool read)
|
||||
{
|
||||
m_read = read;
|
||||
Q_EMIT readChanged(m_read);
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("UPDATE Entries SET read=:read WHERE id=:id AND feed=:feed"));
|
||||
query.bindValue(QStringLiteral(":id"), m_id);
|
||||
query.bindValue(QStringLiteral(":feed"), m_feed->url());
|
||||
query.bindValue(QStringLiteral(":read"), m_read);
|
||||
Database::instance().execute(query);
|
||||
Q_EMIT m_feed->unreadEntryCountChanged();
|
||||
Q_EMIT DataManager::instance().unreadEntryCountChanged(m_feed->url());
|
||||
// TODO: can one of the two slots be removed??
|
||||
if (read != m_read) {
|
||||
// Making a detour through DataManager to make bulk operations more
|
||||
// performant. DataManager will call setReadInternal on every item to
|
||||
// be marked read/unread. So implement features there.
|
||||
DataManager::instance().bulkMarkRead(read, QStringList(m_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Follow up actions
|
||||
if (read && hasEnclosure()) {
|
||||
// 1) Reset play position
|
||||
if (SettingsManager::self()->resetPositionOnPlayed()) {
|
||||
m_enclosure->setPlayPosition(0);
|
||||
}
|
||||
void Entry::setReadInternal(bool read)
|
||||
{
|
||||
if (read != m_read) {
|
||||
// Make sure that operations done here can be wrapped inside an sqlite
|
||||
// transaction. I.e. no calls that trigger a SELECT operation.
|
||||
m_read = read;
|
||||
Q_EMIT readChanged(m_read);
|
||||
|
||||
// 2) Remove item from queue
|
||||
setQueueStatus(false);
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("UPDATE Entries SET read=:read WHERE id=:id AND feed=:feed"));
|
||||
query.bindValue(QStringLiteral(":id"), m_id);
|
||||
query.bindValue(QStringLiteral(":feed"), m_feed->url());
|
||||
query.bindValue(QStringLiteral(":read"), m_read);
|
||||
Database::instance().execute(query);
|
||||
|
||||
// 3) Remove "new" label
|
||||
setNew(false);
|
||||
Q_EMIT m_feed->unreadEntryCountChanged();
|
||||
Q_EMIT DataManager::instance().unreadEntryCountChanged(m_feed->url());
|
||||
// TODO: can one of the two slots be removed??
|
||||
|
||||
// 4) Delete episode if that setting is set
|
||||
if (SettingsManager::self()->autoDeleteOnPlayed() == 1) {
|
||||
m_enclosure->deleteFile();
|
||||
// Follow up actions
|
||||
if (read) {
|
||||
// 1) Remove item from queue
|
||||
setQueueStatusInternal(false);
|
||||
|
||||
// 2) Remove "new" label
|
||||
setNewInternal(false);
|
||||
|
||||
if (hasEnclosure()) {
|
||||
// 3) Reset play position
|
||||
if (SettingsManager::self()->resetPositionOnPlayed()) {
|
||||
m_enclosure->setPlayPosition(0);
|
||||
}
|
||||
|
||||
// 4) Delete episode if that setting is set
|
||||
if (SettingsManager::self()->autoDeleteOnPlayed() == 1) {
|
||||
m_enclosure->deleteFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Entry::setNew(bool state)
|
||||
{
|
||||
m_new = state;
|
||||
Q_EMIT newChanged(m_new);
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("UPDATE Entries SET new=:new WHERE id=:id AND feed=:feed"));
|
||||
query.bindValue(QStringLiteral(":id"), m_id);
|
||||
query.bindValue(QStringLiteral(":feed"), m_feed->url());
|
||||
query.bindValue(QStringLiteral(":new"), m_new);
|
||||
Database::instance().execute(query);
|
||||
// Q_EMIT m_feed->newEntryCountChanged(); // TODO: signal and slots to be implemented
|
||||
Q_EMIT DataManager::instance().newEntryCountChanged(m_feed->url());
|
||||
if (state != m_new) {
|
||||
// Making a detour through DataManager to make bulk operations more
|
||||
// performant. DataManager will call setNewInternal on every item to
|
||||
// be marked new/not new. So implement features there.
|
||||
DataManager::instance().bulkMarkNew(state, QStringList(m_id));
|
||||
}
|
||||
}
|
||||
|
||||
void Entry::setNewInternal(bool state)
|
||||
{
|
||||
if (state != m_new) {
|
||||
// Make sure that operations done here can be wrapped inside an sqlite
|
||||
// transaction. I.e. no calls that trigger a SELECT operation.
|
||||
m_new = state;
|
||||
Q_EMIT newChanged(m_new);
|
||||
|
||||
QSqlQuery query;
|
||||
query.prepare(QStringLiteral("UPDATE Entries SET new=:new WHERE id=:id;"));
|
||||
query.bindValue(QStringLiteral(":id"), m_id);
|
||||
query.bindValue(QStringLiteral(":new"), m_new);
|
||||
Database::instance().execute(query);
|
||||
|
||||
// Q_EMIT m_feed->newEntryCountChanged(); // TODO: signal and slots to be implemented
|
||||
Q_EMIT DataManager::instance().newEntryCountChanged(m_feed->url());
|
||||
}
|
||||
}
|
||||
|
||||
QString Entry::adjustedContent(int width, int fontSize)
|
||||
@ -248,14 +272,30 @@ bool Entry::queueStatus() const
|
||||
void Entry::setQueueStatus(bool state)
|
||||
{
|
||||
if (state != DataManager::instance().entryInQueue(this)) {
|
||||
if (state)
|
||||
DataManager::instance().addToQueue(this);
|
||||
else
|
||||
DataManager::instance().removeQueueItem(this);
|
||||
Q_EMIT queueStatusChanged(state);
|
||||
// Making a detour through DataManager to make bulk operations more
|
||||
// performant. DataManager will call setQueueStatusInternal on every
|
||||
// item to be processed. So implement features there.
|
||||
DataManager::instance().bulkQueueStatus(state, QStringList(m_id));
|
||||
}
|
||||
}
|
||||
|
||||
void Entry::setQueueStatusInternal(bool state)
|
||||
{
|
||||
// Make sure that operations done here can be wrapped inside an sqlite
|
||||
// transaction. I.e. no calls that trigger a SELECT operation.
|
||||
if (state) {
|
||||
DataManager::instance().addToQueue(m_id);
|
||||
// Set status to unplayed/unread when adding item to the queue
|
||||
setReadInternal(false);
|
||||
} else {
|
||||
DataManager::instance().removeFromQueue(m_id);
|
||||
// Unset "new" state
|
||||
setNewInternal(false);
|
||||
}
|
||||
|
||||
Q_EMIT queueStatusChanged(state);
|
||||
}
|
||||
|
||||
void Entry::setImage(const QString &image)
|
||||
{
|
||||
m_image = image;
|
||||
|
@ -69,6 +69,10 @@ public:
|
||||
|
||||
Q_INVOKABLE QString adjustedContent(int width, int fontSize);
|
||||
|
||||
void setNewInternal(bool state);
|
||||
void setReadInternal(bool read);
|
||||
void setQueueStatusInternal(bool state);
|
||||
|
||||
Q_SIGNALS:
|
||||
void readChanged(bool read);
|
||||
void newChanged(bool state);
|
||||
|
@ -41,6 +41,7 @@
|
||||
#include "kasts-version.h"
|
||||
#include "models/downloadmodel.h"
|
||||
#include "models/entriesmodel.h"
|
||||
#include "models/episodemodel.h"
|
||||
#include "models/episodeproxymodel.h"
|
||||
#include "models/errorlogmodel.h"
|
||||
#include "models/feedsmodel.h"
|
||||
@ -126,6 +127,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
qmlRegisterUncreatableType<EntriesModel>("org.kde.kasts", 1, 0, "EntriesModel", 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"));
|
||||
|
||||
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "Fetcher", &Fetcher::instance());
|
||||
qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "Database", &Database::instance());
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "database.h"
|
||||
#include "datamanager.h"
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
DownloadModel::DownloadModel()
|
||||
: QAbstractListModel(nullptr)
|
||||
@ -20,32 +21,30 @@ DownloadModel::DownloadModel()
|
||||
|
||||
QVariant DownloadModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role != 0)
|
||||
return QVariant();
|
||||
if (index.row() < m_downloadingCount) {
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_downloadingIds[index.row()]));
|
||||
} else if (index.row() < m_downloadingCount + m_partiallyDownloadedCount) {
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_partiallyDownloadedIds[index.row() - m_downloadingCount]));
|
||||
} else if (index.row() < m_downloadingCount + m_partiallyDownloadedCount + m_downloadedCount) {
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_downloadedIds[index.row() - m_downloadingCount - m_partiallyDownloadedCount]));
|
||||
} else {
|
||||
qWarning() << "Trying to fetch DownloadModel item outside of valid range; this should never happen";
|
||||
switch (role) {
|
||||
case EpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_entryIds[index.row()]));
|
||||
case EpisodeModel::Roles::IdRole:
|
||||
return QVariant::fromValue(m_entryIds[index.row()]);
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> DownloadModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roleNames;
|
||||
roleNames[0] = "entry";
|
||||
return roleNames;
|
||||
return {
|
||||
{EpisodeModel::Roles::EntryRole, "entry"},
|
||||
{EpisodeModel::Roles::IdRole, "id"},
|
||||
{EpisodeModel::Roles::ReadRole, "read"},
|
||||
{EpisodeModel::Roles::NewRole, "new"},
|
||||
};
|
||||
}
|
||||
|
||||
int DownloadModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
|
||||
return m_downloadingCount + m_partiallyDownloadedCount + m_downloadedCount;
|
||||
return m_entryIds.count();
|
||||
}
|
||||
|
||||
void DownloadModel::monitorDownloadStatus()
|
||||
@ -57,9 +56,7 @@ void DownloadModel::monitorDownloadStatus()
|
||||
|
||||
void DownloadModel::updateInternalState()
|
||||
{
|
||||
m_downloadingIds.clear();
|
||||
m_partiallyDownloadedIds.clear();
|
||||
m_downloadedIds.clear();
|
||||
m_entryIds.clear();
|
||||
|
||||
QSqlQuery query;
|
||||
query.prepare(
|
||||
@ -68,22 +65,24 @@ void DownloadModel::updateInternalState()
|
||||
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloading));
|
||||
Database::instance().execute(query);
|
||||
while (query.next()) {
|
||||
m_downloadingIds += query.value(QStringLiteral("id")).toString();
|
||||
m_entryIds += query.value(QStringLiteral("id")).toString();
|
||||
}
|
||||
|
||||
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::PartiallyDownloaded));
|
||||
Database::instance().execute(query);
|
||||
while (query.next()) {
|
||||
m_partiallyDownloadedIds += query.value(QStringLiteral("id")).toString();
|
||||
m_entryIds += query.value(QStringLiteral("id")).toString();
|
||||
}
|
||||
|
||||
query.bindValue(QStringLiteral(":downloaded"), Enclosure::statusToDb(Enclosure::Downloaded));
|
||||
Database::instance().execute(query);
|
||||
while (query.next()) {
|
||||
m_downloadedIds += query.value(QStringLiteral("id")).toString();
|
||||
m_entryIds += query.value(QStringLiteral("id")).toString();
|
||||
}
|
||||
|
||||
m_downloadingCount = m_downloadingIds.count();
|
||||
m_partiallyDownloadedCount = m_partiallyDownloadedIds.count();
|
||||
m_downloadedCount = m_downloadedIds.count();
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection DownloadModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QItemSelection>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
@ -29,6 +30,8 @@ public:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
public Q_SLOTS:
|
||||
void monitorDownloadStatus();
|
||||
|
||||
@ -40,8 +43,10 @@ private:
|
||||
QStringList m_downloadingIds;
|
||||
QStringList m_partiallyDownloadedIds;
|
||||
QStringList m_downloadedIds;
|
||||
QStringList m_entryIds;
|
||||
|
||||
int m_downloadingCount = 0;
|
||||
int m_partiallyDownloadedCount = 0;
|
||||
int m_downloadedCount = 0;
|
||||
int m_entryCount = 0;
|
||||
};
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
#include "feed.h"
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
EntriesModel::EntriesModel(Feed *feed)
|
||||
: QAbstractListModel(feed)
|
||||
@ -30,17 +31,24 @@ EntriesModel::EntriesModel(Feed *feed)
|
||||
|
||||
QVariant EntriesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role != 0)
|
||||
switch (role) {
|
||||
case EpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_feed, index.row()));
|
||||
case EpisodeModel::Roles::IdRole:
|
||||
return QVariant::fromValue(DataManager::instance().getIdList(m_feed)[index.row()]);
|
||||
default:
|
||||
return QVariant();
|
||||
// qDebug() << "fetching item" << index.row();
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_feed, index.row()));
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> EntriesModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roleNames;
|
||||
roleNames[0] = "entry";
|
||||
return roleNames;
|
||||
return {
|
||||
{EpisodeModel::Roles::EntryRole, "entry"},
|
||||
{EpisodeModel::Roles::IdRole, "id"},
|
||||
{EpisodeModel::Roles::ReadRole, "read"},
|
||||
{EpisodeModel::Roles::NewRole, "new"},
|
||||
};
|
||||
}
|
||||
|
||||
int EntriesModel::rowCount(const QModelIndex &parent) const
|
||||
@ -53,3 +61,9 @@ Feed *EntriesModel::feed() const
|
||||
{
|
||||
return m_feed;
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection EntriesModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QItemSelection>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
@ -28,6 +29,8 @@ public:
|
||||
|
||||
Feed *feed() const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
private:
|
||||
Feed *m_feed;
|
||||
};
|
||||
|
@ -33,6 +33,8 @@ QVariant EpisodeModel::data(const QModelIndex &index, int role) const
|
||||
switch (role) {
|
||||
case EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getEntry(m_entryIds[index.row()]));
|
||||
case IdRole:
|
||||
return QVariant::fromValue(m_entryIds[index.row()]);
|
||||
case ReadRole:
|
||||
return QVariant::fromValue(m_read[index.row()]);
|
||||
case NewRole:
|
||||
@ -46,6 +48,7 @@ QHash<int, QByteArray> EpisodeModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{EntryRole, "entry"},
|
||||
{IdRole, "id"},
|
||||
{ReadRole, "read"},
|
||||
{NewRole, "new"},
|
||||
};
|
||||
|
@ -19,9 +19,11 @@ class EpisodeModel : public QAbstractListModel
|
||||
public:
|
||||
enum Roles {
|
||||
EntryRole = Qt::UserRole,
|
||||
IdRole,
|
||||
ReadRole,
|
||||
NewRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit EpisodeModel();
|
||||
QVariant data(const QModelIndex &index, int role = Qt::UserRole) const override;
|
||||
|
@ -8,6 +8,9 @@
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
|
||||
EpisodeProxyModel::EpisodeProxyModel()
|
||||
: QSortFilterProxyModel(nullptr)
|
||||
{
|
||||
@ -49,13 +52,29 @@ EpisodeProxyModel::FilterType EpisodeProxyModel::filterType() const
|
||||
void EpisodeProxyModel::setFilterType(FilterType type)
|
||||
{
|
||||
if (type != m_currentFilter) {
|
||||
disconnect(&DataManager::instance(), &DataManager::bulkReadStatusActionFinished, this, nullptr);
|
||||
disconnect(&DataManager::instance(), &DataManager::bulkNewStatusActionFinished, this, nullptr);
|
||||
|
||||
beginResetModel();
|
||||
// TODO: Connect to signals to capture new and read updates in case those
|
||||
// filters are active. Also disconnect from signals if the filters are
|
||||
// removed
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -82,3 +101,9 @@ QString EpisodeProxyModel::getFilterName(FilterType type) const
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection EpisodeProxyModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
}
|
||||
|
@ -6,10 +6,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QItemSelection>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
class Entry;
|
||||
|
||||
class EpisodeProxyModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -38,6 +41,8 @@ public:
|
||||
|
||||
Q_INVOKABLE QString getFilterName(FilterType type) const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
Q_SIGNALS:
|
||||
void filterTypeChanged();
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "audiomanager.h"
|
||||
#include "datamanager.h"
|
||||
#include "entry.h"
|
||||
#include "models/episodemodel.h"
|
||||
|
||||
QueueModel::QueueModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
@ -47,17 +48,24 @@ QueueModel::QueueModel(QObject *parent)
|
||||
|
||||
QVariant QueueModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (role != 0)
|
||||
switch (role) {
|
||||
case EpisodeModel::Roles::EntryRole:
|
||||
return QVariant::fromValue(DataManager::instance().getQueueEntry(index.row()));
|
||||
case EpisodeModel::Roles::IdRole:
|
||||
return QVariant::fromValue(DataManager::instance().queue()[index.row()]);
|
||||
default:
|
||||
return QVariant();
|
||||
qCDebug(kastsQueueModel) << "return entry" << DataManager::instance().getQueueEntry(index.row());
|
||||
return QVariant::fromValue(DataManager::instance().getQueueEntry(index.row()));
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> QueueModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roleNames;
|
||||
roleNames[0] = "entry";
|
||||
return roleNames;
|
||||
return {
|
||||
{EpisodeModel::Roles::EntryRole, "entry"},
|
||||
{EpisodeModel::Roles::IdRole, "id"},
|
||||
{EpisodeModel::Roles::ReadRole, "read"},
|
||||
{EpisodeModel::Roles::NewRole, "new"},
|
||||
};
|
||||
}
|
||||
|
||||
int QueueModel::rowCount(const QModelIndex &parent) const
|
||||
@ -86,3 +94,9 @@ QString QueueModel::formattedTimeLeft() const
|
||||
static KFormat format;
|
||||
return format.formatDuration(timeLeft());
|
||||
}
|
||||
|
||||
// Hack to get a QItemSelection in QML
|
||||
QItemSelection QueueModel::createSelection(int rowa, int rowb)
|
||||
{
|
||||
return QItemSelection(index(rowa, 0), index(rowb, 0));
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QItemSelection>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
@ -36,6 +37,8 @@ public:
|
||||
int timeLeft() const;
|
||||
QString formattedTimeLeft() const;
|
||||
|
||||
Q_INVOKABLE QItemSelection createSelection(int rowa, int rowb);
|
||||
|
||||
Q_SIGNALS:
|
||||
void timeLeftChanged();
|
||||
};
|
||||
|
@ -33,6 +33,8 @@ Kirigami.ScrollablePage {
|
||||
visible: !Kirigami.Settings.isMobile
|
||||
}
|
||||
|
||||
contextualActions: episodeList.defaultActionList
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
visible: episodeList.count === 0
|
||||
|
||||
@ -50,10 +52,14 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
GenericEntryListView {
|
||||
id: episodeList
|
||||
visible: count !== 0
|
||||
isDownloads: true
|
||||
reuseItems: true
|
||||
|
||||
model: DownloadModel
|
||||
delegate: episodeListDelegate
|
||||
|
||||
section {
|
||||
delegate: Kirigami.ListSectionHeader {
|
||||
@ -65,10 +71,5 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
property: "entry.enclosure.status"
|
||||
}
|
||||
|
||||
delegate: Kirigami.DelegateRecycler {
|
||||
width: episodeList.width
|
||||
sourceComponent: episodeListDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.14 as Controls
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtGraphicalEffects 1.15
|
||||
@ -49,6 +49,13 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
iconName: "view-refresh"
|
||||
text: i18n("Refresh Podcast")
|
||||
onTriggered: page.refreshing = true
|
||||
visible: !Kirigami.Settings.isMobile || entryList.count === 0
|
||||
}
|
||||
|
||||
contextualActions: [
|
||||
Kirigami.Action {
|
||||
iconName: "help-about-symbolic"
|
||||
@ -70,11 +77,12 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
]
|
||||
|
||||
actions.main: Kirigami.Action {
|
||||
iconName: "view-refresh"
|
||||
text: i18n("Refresh Podcast")
|
||||
onTriggered: page.refreshing = true
|
||||
visible: !Kirigami.Settings.isMobile || entryList.count === 0
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
@ -94,15 +102,13 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
GenericEntryListView {
|
||||
id: entryList
|
||||
visible: count !== 0
|
||||
model: page.feed.entries
|
||||
reuseItems: true
|
||||
|
||||
delegate: Kirigami.DelegateRecycler {
|
||||
width: entryList.width
|
||||
sourceComponent: entryListDelegate
|
||||
}
|
||||
model: page.feed.entries
|
||||
delegate: entryListDelegate
|
||||
|
||||
// OverlayHeader looks nicer, but seems completely broken when flicking the list
|
||||
// headerPositioning: ListView.OverlayHeader
|
||||
|
@ -47,7 +47,7 @@ Kirigami.ScrollablePage {
|
||||
|
||||
ListView {
|
||||
// TODO: fix automatic width
|
||||
implicitWidth: Kirigami.Units.gridUnit * 12
|
||||
implicitWidth: Kirigami.Units.gridUnit * 15
|
||||
clip: true
|
||||
|
||||
model: ListModel {
|
||||
@ -105,6 +105,8 @@ Kirigami.ScrollablePage {
|
||||
]
|
||||
}
|
||||
|
||||
contextualActions: episodeList.defaultActionList
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
visible: episodeList.count === 0
|
||||
|
||||
@ -125,12 +127,14 @@ Kirigami.ScrollablePage {
|
||||
id: episodeProxyModel
|
||||
}
|
||||
|
||||
ListView {
|
||||
GenericEntryListView {
|
||||
id: episodeList
|
||||
anchors.fill: parent
|
||||
visible: count !== 0
|
||||
model: episodeProxyModel
|
||||
|
||||
// TODO: seems like reuseItems is way too slow; using DelegateRecycler
|
||||
// for now; still have to find out why...
|
||||
delegate: Kirigami.DelegateRecycler {
|
||||
width: episodeList.width
|
||||
sourceComponent: episodeListDelegate
|
||||
|
@ -4,11 +4,12 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.14 as Controls
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtMultimedia 5.15
|
||||
import QtGraphicalEffects 1.15
|
||||
import QtQml.Models 2.15
|
||||
|
||||
import org.kde.kirigami 2.14 as Kirigami
|
||||
|
||||
@ -20,7 +21,12 @@ Kirigami.SwipeListItem {
|
||||
|
||||
property bool isQueue: false
|
||||
property bool isDownloads: false
|
||||
property var listView: ""
|
||||
property QtObject listView: undefined
|
||||
property bool selected: false
|
||||
property int row: model ? model.row : -1
|
||||
|
||||
highlighted: selected
|
||||
activeBackgroundColor: Qt.lighter(Kirigami.Theme.highlightColor, 1.3)
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: entry.title
|
||||
@ -30,143 +36,223 @@ Kirigami.SwipeListItem {
|
||||
|
||||
Keys.onReturnPressed: clicked()
|
||||
|
||||
contentItem: RowLayout {
|
||||
// We need to update the "selected" status:
|
||||
// - if the selected indexes changes
|
||||
// - if our delegate moves
|
||||
// - if the model moves and the delegate stays in the same place
|
||||
function updateIsSelected() {
|
||||
selected = listView.selectionModel.rowIntersectsSelection(row);
|
||||
}
|
||||
|
||||
Loader {
|
||||
property var loaderListView: listView
|
||||
property var loaderListItem: listItem
|
||||
sourceComponent: dragHandleComponent
|
||||
active: isQueue
|
||||
}
|
||||
onRowChanged: {
|
||||
updateIsSelected();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dragHandleComponent
|
||||
Kirigami.ListItemDragHandle {
|
||||
listItem: loaderListItem
|
||||
listView: loaderListView
|
||||
onMoveRequested: DataManager.moveQueueItem(oldIndex, newIndex)
|
||||
Component.onCompleted: {
|
||||
updateIsSelected();
|
||||
}
|
||||
|
||||
contentItem: MouseArea {
|
||||
id: mouseArea
|
||||
implicitHeight: rowLayout.implicitHeight
|
||||
implicitWidth: rowLayout.implicitWidth
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: {
|
||||
// Keep track of (currently) selected items
|
||||
var modelIndex = listItem.listView.model.index(index, 0);
|
||||
|
||||
if (listView.selectionModel.isSelected(modelIndex) && mouse.button == Qt.RightButton) {
|
||||
listView.contextMenu.popup(null, mouse.x+1, mouse.y+1);
|
||||
} else if (mouse.modifiers & Qt.ShiftModifier) {
|
||||
// Have to take a detour through c++ since selecting large sets
|
||||
// in QML is extremely slow
|
||||
listView.selectionModel.select(listView.model.createSelection(modelIndex.row, listView.selectionModel.currentIndex.row), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
} else if (mouse.modifiers & Qt.ControlModifier) {
|
||||
listView.selectionModel.select(modelIndex, ItemSelectionModel.Toggle | ItemSelectionModel.Rows);
|
||||
} else if (mouse.button == Qt.LeftButton) {
|
||||
listView.currentIndex = index;
|
||||
listView.selectionModel.setCurrentIndex(modelIndex, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
listItem.clicked();
|
||||
} else if (mouse.button == Qt.RightButton) {
|
||||
// This item is right-clicked, but isn't selected
|
||||
listView.selectionForContextMenu = [modelIndex];
|
||||
listView.contextMenu.popup(null, mouse.x+1, mouse.y+1);
|
||||
}
|
||||
}
|
||||
|
||||
ImageWithFallback {
|
||||
id: img
|
||||
imageSource: entry.cachedImage
|
||||
property int size: Kirigami.Units.gridUnit * 3
|
||||
Layout.preferredHeight: size
|
||||
Layout.preferredWidth: size
|
||||
Layout.rightMargin:Kirigami.Units.smallSpacing
|
||||
fractionalRadius: 1.0 / 8.0
|
||||
onPressAndHold: {
|
||||
var modelIndex = listItem.listView.model.index(index, 0);
|
||||
listView.selectionModel.select(modelIndex, ItemSelectionModel.Toggle | ItemSelectionModel.Rows);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
RowLayout{
|
||||
Kirigami.Icon {
|
||||
Layout.maximumHeight: playedLabel.implicitHeight
|
||||
Layout.maximumWidth: playedLabel.implicitHeight
|
||||
source: "checkbox"
|
||||
visible: entry.read
|
||||
}
|
||||
Controls.Label {
|
||||
id: playedLabel
|
||||
text: (entry.enclosure ? i18n("Played") : i18n("Read")) + " ·"
|
||||
font: Kirigami.Theme.smallFont
|
||||
visible: entry.read
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.Label {
|
||||
text: entry.new ? i18n("New") + " ·" : ""
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: Kirigami.Theme.highlightColor
|
||||
visible: entry.new
|
||||
opacity: 0.7
|
||||
}
|
||||
Kirigami.Icon {
|
||||
Layout.maximumHeight: 0.8 * supertitle.implicitHeight
|
||||
Layout.maximumWidth: 0.8 * supertitle.implicitHeight
|
||||
source: "source-playlist"
|
||||
visible: !isQueue && entry.queueStatus
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.Label {
|
||||
id: supertitle
|
||||
text: (!isQueue && entry.queueStatus ? "· " : "") + entry.updated.toLocaleDateString(Qt.locale(), Locale.NarrowFormat) + (entry.enclosure ? ( entry.enclosure.size !== 0 ? " · " + entry.enclosure.formattedSize : "") : "" )
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
Connections {
|
||||
target: listView.selectionModel
|
||||
function onSelectionChanged() {
|
||||
updateIsSelected();
|
||||
}
|
||||
Controls.Label {
|
||||
text: entry.title
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onLayoutChanged() {
|
||||
updateIsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.fill: parent
|
||||
|
||||
Loader {
|
||||
sourceComponent: entry.enclosure && (entry.enclosure.status === Enclosure.Downloading || (isDownloads && entry.enclosure.status === Enclosure.PartiallyDownloaded)) ? downloadProgress : ( entry.enclosure && entry.enclosure.playPosition > 0 ? playProgress : subtitle)
|
||||
Layout.fillWidth: true
|
||||
property var loaderListView: listView
|
||||
property var loaderListItem: listItem
|
||||
sourceComponent: dragHandleComponent
|
||||
active: isQueue
|
||||
}
|
||||
|
||||
Component {
|
||||
id: subtitle
|
||||
id: dragHandleComponent
|
||||
Kirigami.ListItemDragHandle {
|
||||
listItem: loaderListItem
|
||||
listView: loaderListView
|
||||
onMoveRequested: {
|
||||
DataManager.moveQueueItem(oldIndex, newIndex);
|
||||
// reset current selection when moving items
|
||||
var modelIndex = listItem.listView.model.index(newIndex, 0);
|
||||
listView.currentIndex = newIndex;
|
||||
listView.selectionModel.setCurrentIndex(modelIndex, ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageWithFallback {
|
||||
id: img
|
||||
imageSource: entry.cachedImage
|
||||
property int size: Kirigami.Units.gridUnit * 3
|
||||
Layout.preferredHeight: size
|
||||
Layout.preferredWidth: size
|
||||
Layout.rightMargin: Kirigami.Units.smallSpacing
|
||||
fractionalRadius: 1.0 / 8.0
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
RowLayout{
|
||||
Kirigami.Icon {
|
||||
Layout.maximumHeight: playedLabel.implicitHeight
|
||||
Layout.maximumWidth: playedLabel.implicitHeight
|
||||
source: "checkbox"
|
||||
visible: entry.read
|
||||
}
|
||||
Controls.Label {
|
||||
id: playedLabel
|
||||
text: (entry.enclosure ? i18n("Played") : i18n("Read")) + " ·"
|
||||
font: Kirigami.Theme.smallFont
|
||||
visible: entry.read
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.Label {
|
||||
text: entry.new ? i18n("New") + " ·" : ""
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: Kirigami.Theme.highlightColor
|
||||
visible: entry.new
|
||||
opacity: 0.7
|
||||
}
|
||||
Kirigami.Icon {
|
||||
Layout.maximumHeight: 0.8 * supertitle.implicitHeight
|
||||
Layout.maximumWidth: 0.8 * supertitle.implicitHeight
|
||||
source: "source-playlist"
|
||||
visible: !isQueue && entry.queueStatus
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.Label {
|
||||
id: supertitle
|
||||
text: (!isQueue && entry.queueStatus ? "· " : "") + entry.updated.toLocaleDateString(Qt.locale(), Locale.NarrowFormat) + (entry.enclosure ? ( entry.enclosure.size !== 0 ? " · " + entry.enclosure.formattedSize : "") : "" )
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
Controls.Label {
|
||||
text: entry.enclosure ? entry.enclosure.formattedDuration : ""
|
||||
text: entry.title
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
visible: !downloadProgress.visible
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: downloadProgress
|
||||
RowLayout {
|
||||
Loader {
|
||||
sourceComponent: entry.enclosure && (entry.enclosure.status === Enclosure.Downloading || (isDownloads && entry.enclosure.status === Enclosure.PartiallyDownloaded)) ? downloadProgress : ( entry.enclosure && entry.enclosure.playPosition > 0 ? playProgress : subtitle)
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Component {
|
||||
id: subtitle
|
||||
Controls.Label {
|
||||
text: entry.enclosure.formattedDownloadSize
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.ProgressBar {
|
||||
from: 0
|
||||
to: 1
|
||||
value: entry.enclosure.downloadProgress
|
||||
text: entry.enclosure ? entry.enclosure.formattedDuration : ""
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Controls.Label {
|
||||
text: entry.enclosure.formattedSize
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
visible: !downloadProgress.visible
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: downloadProgress
|
||||
RowLayout {
|
||||
Controls.Label {
|
||||
text: entry.enclosure.formattedDownloadSize
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.ProgressBar {
|
||||
from: 0
|
||||
to: 1
|
||||
value: entry.enclosure.downloadProgress
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Controls.Label {
|
||||
text: entry.enclosure.formattedSize
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: playProgress
|
||||
RowLayout {
|
||||
Controls.Label {
|
||||
text: entry.enclosure.formattedPlayPosition
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.ProgressBar {
|
||||
from: 0
|
||||
to: entry.enclosure.duration
|
||||
value: entry.enclosure.playPosition / 1000
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Controls.Label {
|
||||
text: (SettingsManager.toggleRemainingTime)
|
||||
? "-" + entry.enclosure.formattedLeftDuration
|
||||
: entry.enclosure.formattedDuration
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: playProgress
|
||||
RowLayout {
|
||||
Controls.Label {
|
||||
text: entry.enclosure.formattedPlayPosition
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
Controls.ProgressBar {
|
||||
from: 0
|
||||
to: entry.enclosure.duration
|
||||
value: entry.enclosure.playPosition / 1000
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Controls.Label {
|
||||
text: (SettingsManager.toggleRemainingTime)
|
||||
? "-" + entry.enclosure.formattedLeftDuration
|
||||
: entry.enclosure.formattedDuration
|
||||
elide: Text.ElideRight
|
||||
font: Kirigami.Theme.smallFont
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
Controls.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Controls.MenuItem {
|
||||
action: listView.addToQueueAction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
267
src/qml/GenericEntryListView.qml
Normal file
267
src/qml/GenericEntryListView.qml
Normal file
@ -0,0 +1,267 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2021 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.14 as Controls
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQml.Models 2.15
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
import org.kde.kasts 1.0
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
visible: count !== 0
|
||||
property bool isQueue: false
|
||||
property bool isDownloads: false
|
||||
|
||||
property var selectionForContextMenu: []
|
||||
property var singleSelectedEntry: undefined
|
||||
property ItemSelectionModel selectionModel: ItemSelectionModel {
|
||||
id: selectionModel
|
||||
model: listView.model
|
||||
onSelectionChanged: {
|
||||
selectionForContextMenu = selectedIndexes;
|
||||
}
|
||||
}
|
||||
|
||||
onSelectionForContextMenuChanged: {
|
||||
if (selectionForContextMenu.length === 1) {
|
||||
singleSelectedEntry = selectionForContextMenu[0].model.data(selectionForContextMenu[0], EpisodeModel.EntryRole);
|
||||
} else {
|
||||
singleSelectedEntry = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// The selection is not updated when the model is reset, so we have to take
|
||||
// this into account manually.
|
||||
// TODO: Fix the fact that the current item is not highlighted after reset
|
||||
Connections {
|
||||
target: listView.model
|
||||
function onModelAboutToBeReset() {
|
||||
startOfSelection = -1;
|
||||
selectionForContextMenu = [];
|
||||
listView.selectionModel.clear();
|
||||
listView.selectionModel.setCurrentIndex(model.index(0, 0), ItemSelectionModel.Current); // Only set current item; don't select it
|
||||
currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.matches(StandardKey.SelectAll)) {
|
||||
listView.selectionModel.select(model.index(0, 0), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Columns);
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_Up:
|
||||
selectRelative(-1, event.modifiers == Qt.ShiftModifier);
|
||||
return;
|
||||
case Qt.Key_Down:
|
||||
selectRelative(1, event.modifiers == Qt.ShiftModifier);
|
||||
return;
|
||||
case Qt.Key_PageUp:
|
||||
if (!atYBeginning) {
|
||||
if ((contentY - listView.height) < 0) {
|
||||
contentY = 0
|
||||
} else {
|
||||
contentY -= listView.height
|
||||
}
|
||||
returnToBounds()
|
||||
}
|
||||
return;
|
||||
case Qt.Key_PageDown:
|
||||
if (!atYEnd) {
|
||||
if ((contentY + listView.height) > contentHeight - height) {
|
||||
contentY = contentHeight - height
|
||||
} else {
|
||||
contentY += listView.height
|
||||
}
|
||||
returnToBounds()
|
||||
}
|
||||
return;
|
||||
case Qt.Key_Home:
|
||||
if (!atYBeginning) {
|
||||
contentY = 0
|
||||
returnToBounds()
|
||||
}
|
||||
return;
|
||||
case Qt.Key_End:
|
||||
if (!atYEnd) {
|
||||
contentY = contentHeight - height
|
||||
returnToBounds()
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus && !selectionModel.hasSelection) {
|
||||
selectionModel.clear();
|
||||
selectionModel.setCurrentIndex(model.index(0, 0), ItemSelectionModel.Current); // Only set current item; don't select it
|
||||
}
|
||||
}
|
||||
|
||||
function selectRelative(delta, append) {
|
||||
var nextRow = listView.currentIndex + delta;
|
||||
if (nextRow < 0) {
|
||||
nextRow = 0;
|
||||
}
|
||||
if (nextRow >= listView.count) {
|
||||
nextRow = listView.count - 1;
|
||||
}
|
||||
if (append) {
|
||||
listView.selectionModel.select(listView.model.createSelection(nextRow, listView.selectionModel.currentIndex.row), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
} else {
|
||||
listView.selectionModel.setCurrentIndex(model.index(nextRow, 0), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows);
|
||||
}
|
||||
}
|
||||
|
||||
// For lack of a better place, we put generic entry list actions here so
|
||||
// they can be re-used across the different ListViews.
|
||||
property var selectAllAction: Kirigami.Action {
|
||||
iconName: "edit-select-all"
|
||||
text: i18n("Select All")
|
||||
visible: true
|
||||
onTriggered: {
|
||||
listView.selectionModel.select(model.index(0, 0), ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Columns);
|
||||
}
|
||||
}
|
||||
|
||||
property var selectNoneAction: Kirigami.Action {
|
||||
iconName: "edit-select-none"
|
||||
text: i18n("Deselect All")
|
||||
visible: listView.selectionModel.hasSelection
|
||||
onTriggered: {
|
||||
listView.selectionModel.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
property var addToQueueAction: Kirigami.Action {
|
||||
text: i18n("Add to Queue")
|
||||
icon.name: "media-playlist-append"
|
||||
visible: listView.selectionModel.hasSelection && !listView.isQueue && (singleSelectedEntry ? !singleSelectedEntry.queueStatus : true)
|
||||
//visible: listView.selectionModel.hasSelection && !listView.isQueue
|
||||
onTriggered: {
|
||||
DataManager.bulkQueueStatusByIndex(true, selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var removeFromQueueAction: Kirigami.Action {
|
||||
text: i18n("Remove from Queue")
|
||||
icon.name: "list-remove"
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? singleSelectedEntry.queueStatus : true)
|
||||
//visible: listView.selectionModel.hasSelection
|
||||
onTriggered: {
|
||||
DataManager.bulkQueueStatusByIndex(false, selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var markPlayedAction: Kirigami.Action {
|
||||
text: i18n("Mark as Played")
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? !singleSelectedEntry.read : true)
|
||||
onTriggered: {
|
||||
DataManager.bulkMarkReadByIndex(true, selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var markNotPlayedAction: Kirigami.Action {
|
||||
text: i18n("Mark as Unplayed")
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? singleSelectedEntry.read : true)
|
||||
onTriggered: {
|
||||
DataManager.bulkMarkReadByIndex(false, selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var markNewAction: Kirigami.Action {
|
||||
text: i18n("Label as \"New\"")
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? !singleSelectedEntry.new : true)
|
||||
onTriggered: {
|
||||
DataManager.bulkMarkNewByIndex(true, selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var markNotNewAction: Kirigami.Action {
|
||||
text: i18n("Remove \"New\" Label")
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? singleSelectedEntry.new : true)
|
||||
onTriggered: {
|
||||
DataManager.bulkMarkNewByIndex(false, selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var downloadEnclosureAction: Kirigami.Action {
|
||||
text: i18n("Download")
|
||||
icon.name: "download"
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? (singleSelectedEntry.hasEnclosure ? singleSelectedEntry.enclosure.status !== Enclosure.Downloaded : false) : true)
|
||||
onTriggered: {
|
||||
downloadOverlay.selection = selectionForContextMenu;
|
||||
downloadOverlay.run();
|
||||
}
|
||||
}
|
||||
|
||||
property var deleteEnclosureAction: Kirigami.Action {
|
||||
text: i18n("Delete Download")
|
||||
icon.name: "delete"
|
||||
visible: listView.selectionModel.hasSelection && (singleSelectedEntry ? (singleSelectedEntry.hasEnclosure ? singleSelectedEntry.enclosure.status !== Enclosure.Downloadable : false) : true)
|
||||
onTriggered: {
|
||||
DataManager.bulkDeleteEnclosuresByIndex(selectionForContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
property var defaultActionList: [addToQueueAction,
|
||||
removeFromQueueAction,
|
||||
markPlayedAction,
|
||||
markNotPlayedAction,
|
||||
markNewAction,
|
||||
markNotNewAction,
|
||||
downloadEnclosureAction,
|
||||
deleteEnclosureAction,
|
||||
selectAllAction,
|
||||
selectNoneAction]
|
||||
|
||||
property Controls.Menu contextMenu: Controls.Menu {
|
||||
id: contextMenu
|
||||
|
||||
Controls.MenuItem {
|
||||
action: listView.addToQueueAction
|
||||
visible: !listView.isQueue && (singleSelectedEntry ? !singleSelectedEntry.queueStatus : true)
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.removeFromQueueAction
|
||||
visible: singleSelectedEntry ? singleSelectedEntry.queueStatus : true
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.markPlayedAction
|
||||
visible: singleSelectedEntry ? !singleSelectedEntry.read : true
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.markNotPlayedAction
|
||||
visible: singleSelectedEntry ? singleSelectedEntry.read : true
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.markNewAction
|
||||
visible: singleSelectedEntry ? !singleSelectedEntry.new : true
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.markNotNewAction
|
||||
visible: singleSelectedEntry ? singleSelectedEntry.new : true
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.downloadEnclosureAction
|
||||
visible: singleSelectedEntry ? (singleSelectedEntry.hasEnclosure ? singleSelectedEntry.enclosure.status !== Enclosure.Downloaded : false) : true
|
||||
}
|
||||
Controls.MenuItem {
|
||||
action: listView.deleteEnclosureAction
|
||||
visible: singleSelectedEntry ? (singleSelectedEntry.hasEnclosure ? singleSelectedEntry.enclosure.status !== Enclosure.Downloadable : false) : true
|
||||
}
|
||||
onClosed: {
|
||||
// reset to normal selection if this context menu is closed
|
||||
listView.selectionForContextMenu = listView.selectionModel.selectedIndexes;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,10 @@
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
import QtQuick 2.14
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.14 as Controls
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQml.Models 2.15
|
||||
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
|
||||
@ -34,6 +35,8 @@ Kirigami.ScrollablePage {
|
||||
visible: !Kirigami.Settings.isMobile || queueList.count === 0
|
||||
}
|
||||
|
||||
contextualActions: queueList.defaultActionList
|
||||
|
||||
Kirigami.PlaceholderMessage {
|
||||
visible: queueList.count === 0
|
||||
|
||||
@ -51,8 +54,9 @@ Kirigami.ScrollablePage {
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
GenericEntryListView {
|
||||
id: queueList
|
||||
isQueue: true
|
||||
visible: count !== 0
|
||||
anchors.fill: parent
|
||||
|
||||
|
@ -294,10 +294,17 @@ Kirigami.ApplicationWindow {
|
||||
headingText: i18n("Podcast downloads are currently not allowed on metered connections")
|
||||
condition: SettingsManager.allowMeteredEpisodeDownloads
|
||||
property var entry: undefined
|
||||
property var selection: undefined
|
||||
|
||||
function action() {
|
||||
entry.queueStatus = true;
|
||||
entry.enclosure.download();
|
||||
if (selection) {
|
||||
DataManager.bulkDownloadEnclosuresByIndex(selection);
|
||||
} else if (entry) {
|
||||
entry.queueStatus = true;
|
||||
entry.enclosure.download();
|
||||
}
|
||||
selection = undefined;
|
||||
entry = undefined;
|
||||
}
|
||||
|
||||
function allowOnceAction() {
|
||||
|
@ -20,6 +20,7 @@
|
||||
<file alias="ErrorListOverlay.qml">qml/ErrorListOverlay.qml</file>
|
||||
<file alias="GenericHeader.qml">qml/GenericHeader.qml</file>
|
||||
<file alias="GenericEntryDelegate.qml">qml/GenericEntryDelegate.qml</file>
|
||||
<file alias="GenericEntryListView.qml">qml/GenericEntryListView.qml</file>
|
||||
<file alias="DiscoverPage.qml">qml/DiscoverPage.qml</file>
|
||||
<file alias="ImageWithFallback.qml">qml/ImageWithFallback.qml</file>
|
||||
<file alias="UpdateNotification.qml">qml/UpdateNotification.qml</file>
|
||||
|
Loading…
x
Reference in New Issue
Block a user