1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2018 - 2019 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19 
20 #include "FeedsContentsWidget.h"
21 #include "../../../core/Application.h"
22 #include "../../../core/BookmarksManager.h"
23 #include "../../../core/ThemesManager.h"
24 #include "../../../ui/Action.h"
25 #include "../../../ui/Animation.h"
26 #include "../../../ui/FeedPropertiesDialog.h"
27 #include "../../../ui/MainWindow.h"
28 
29 #include "ui_FeedsContentsWidget.h"
30 
31 #include <QtGui/QDesktopServices>
32 #include <QtWidgets/QDesktopWidget>
33 #include <QtWidgets/QInputDialog>
34 #include <QtWidgets/QMenu>
35 #include <QtWidgets/QMessageBox>
36 #include <QtWidgets/QToolButton>
37 #include <QtWidgets/QToolTip>
38 
39 namespace Otter
40 {
41 
EntryDelegate(QObject * parent)42 EntryDelegate::EntryDelegate(QObject *parent) : ItemDelegate(parent)
43 {
44 }
45 
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const46 void EntryDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
47 {
48 	ItemDelegate::initStyleOption(option, index);
49 
50 	if (index.sibling(index.row(), 0).data(FeedsContentsWidget::LastReadTimeRole).isNull())
51 	{
52 		option->font.setBold(true);
53 	}
54 }
55 
FeedDelegate(QObject * parent)56 FeedDelegate::FeedDelegate(QObject *parent) : ItemDelegate(parent)
57 {
58 }
59 
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const60 void FeedDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
61 {
62 	ItemDelegate::initStyleOption(option, index);
63 
64 	if (index.data(FeedsModel::IsUpdatingRole).toBool())
65 	{
66 		const Animation *animation(FeedsContentsWidget::getUpdateAnimation());
67 
68 		if (animation)
69 		{
70 			option->icon = QIcon(animation->getCurrentPixmap());
71 		}
72 	}
73 }
74 
75 Animation* FeedsContentsWidget::m_updateAnimation = nullptr;
76 
FeedsContentsWidget(const QVariantMap & parameters,QWidget * parent)77 FeedsContentsWidget::FeedsContentsWidget(const QVariantMap &parameters, QWidget *parent) : ContentsWidget(parameters, nullptr, parent),
78 	m_feed(nullptr),
79 	m_feedModel(nullptr),
80 	m_ui(new Ui::FeedsContentsWidget)
81 {
82 	m_ui->setupUi(this);
83 	m_ui->subscribeFeedWidget->hide();
84 	m_ui->feedsHorizontalSplitterWidget->setSizes({300, qMax(500, (width() - 300))});
85 	m_ui->entriesFilterLineEditWidget->setClearOnEscape(true);
86 	m_ui->entriesViewWidget->setItemDelegate(new EntryDelegate(this));
87 	m_ui->entriesViewWidget->installEventFilter(this);
88 	m_ui->entriesViewWidget->viewport()->installEventFilter(this);
89 	m_ui->entriesViewWidget->viewport()->setMouseTracking(true);
90 	m_ui->feedsFilterLineEditWidget->setClearOnEscape(true);
91 	m_ui->feedsViewWidget->setViewMode(ItemViewWidget::TreeView);
92 	m_ui->feedsViewWidget->setModel(FeedsManager::getModel());
93 	m_ui->feedsViewWidget->setItemDelegate(new FeedDelegate(this));
94 	m_ui->feedsViewWidget->expandAll();
95 	m_ui->feedsViewWidget->installEventFilter(this);
96 	m_ui->feedsViewWidget->viewport()->installEventFilter(this);
97 	m_ui->feedsViewWidget->viewport()->setMouseTracking(true);
98 	m_ui->emailButton->setIcon(ThemesManager::createIcon(QLatin1String("mail-send")));
99 	m_ui->urlButton->setIcon(ThemesManager::createIcon(QLatin1String("text-html")));
100 	m_ui->textBrowserWidget->setOpenExternalLinks(true);
101 
102 	if (isSidebarPanel())
103 	{
104 		m_ui->entriesWidget->hide();
105 		m_ui->entryWidget->hide();
106 	}
107 
108 	connect(FeedsManager::getInstance(), &FeedsManager::feedModified, this, &FeedsContentsWidget::handleFeedModified);
109 	connect(m_ui->entriesFilterLineEditWidget, &LineEditWidget::textChanged, m_ui->entriesViewWidget, &ItemViewWidget::setFilterString);
110 	connect(m_ui->entriesViewWidget, &ItemViewWidget::doubleClicked, this, &FeedsContentsWidget::openEntry);
111 	connect(m_ui->entriesViewWidget, &ItemViewWidget::customContextMenuRequested, this, &FeedsContentsWidget::showEntriesContextMenu);
112 	connect(m_ui->entriesViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateEntry);
113 	connect(m_ui->feedsFilterLineEditWidget, &LineEditWidget::textChanged, m_ui->feedsViewWidget, &ItemViewWidget::setFilterString);
114 	connect(m_ui->feedsViewWidget, &ItemViewWidget::doubleClicked, this, &FeedsContentsWidget::openFeed);
115 	connect(m_ui->feedsViewWidget, &ItemViewWidget::customContextMenuRequested, this, &FeedsContentsWidget::showFeedsContextMenu);
116 	connect(m_ui->feedsViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateActions);
117 	connect(m_ui->okButton, &QToolButton::clicked, this, &FeedsContentsWidget::subscribeFeed);
118 	connect(m_ui->cancelButton, &QToolButton::clicked, m_ui->subscribeFeedWidget, &QWidget::hide);
119 	connect(m_ui->emailButton, &QToolButton::clicked, [&]()
120 	{
121 		const QModelIndex index(m_ui->entriesViewWidget->currentIndex());
122 
123 		QDesktopServices::openUrl(QLatin1String("mailto:") + index.sibling(index.row(), 0).data(EmailRole).toString());
124 	});
125 	connect(m_ui->urlButton, &QToolButton::clicked, [&]()
126 	{
127 		const QModelIndex index(m_ui->entriesViewWidget->currentIndex());
128 
129 		Application::getInstance()->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), index.sibling(index.row(), 0).data(UrlRole).toString()}}, this);
130 	});
131 }
132 
~FeedsContentsWidget()133 FeedsContentsWidget::~FeedsContentsWidget()
134 {
135 	delete m_ui;
136 }
137 
changeEvent(QEvent * event)138 void FeedsContentsWidget::changeEvent(QEvent *event)
139 {
140 	ContentsWidget::changeEvent(event);
141 
142 	if (event->type() == QEvent::LanguageChange)
143 	{
144 		m_ui->retranslateUi(this);
145 
146 		if (m_feedModel)
147 		{
148 			m_feedModel->setHorizontalHeaderLabels({tr("Title"), tr("From"), tr("Published")});
149 		}
150 	}
151 }
152 
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)153 void FeedsContentsWidget::triggerAction(int identifier, const QVariantMap &parameters, ActionsManager::TriggerType trigger)
154 {
155 	switch (identifier)
156 	{
157 		case ActionsManager::ReloadAction:
158 			if (m_feed && !m_feed->isUpdating())
159 			{
160 				m_feed->update();
161 			}
162 
163 			break;
164 		case ActionsManager::DeleteAction:
165 			if (m_ui->feedsViewWidget->hasFocus())
166 			{
167 				removeFeed();
168 			}
169 			else if (m_ui->entriesViewWidget->hasFocus())
170 			{
171 				removeEntry();
172 			}
173 
174 			break;
175 		default:
176 			ContentsWidget::triggerAction(identifier, parameters, trigger);
177 
178 			break;
179 	}
180 }
181 
addFeed()182 void FeedsContentsWidget::addFeed()
183 {
184 	FeedPropertiesDialog dialog(nullptr, findFolder(m_ui->feedsViewWidget->currentIndex()), this);
185 
186 	if (dialog.exec() == QDialog::Rejected)
187 	{
188 		return;
189 	}
190 
191 	if (BookmarksManager::getModel()->hasFeed(dialog.getFeed()->getUrl()) || FeedsManager::getModel()->hasFeed(dialog.getFeed()->getUrl()))
192 	{
193 		QMessageBox messageBox;
194 		messageBox.setWindowTitle(tr("Question"));
195 		messageBox.setText(tr("You already subscribed this feed."));
196 		messageBox.setInformativeText(tr("Do you want to continue?"));
197 		messageBox.setIcon(QMessageBox::Question);
198 		messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
199 		messageBox.setDefaultButton(QMessageBox::Yes);
200 
201 		if (messageBox.exec() == QMessageBox::Cancel)
202 		{
203 			return;
204 		}
205 	}
206 
207 	FeedsManager::getModel()->addEntry(dialog.getFeed(), dialog.getFolder());
208 
209 	updateActions();
210 }
211 
addFolder()212 void FeedsContentsWidget::addFolder()
213 {
214 	const QString title(QInputDialog::getText(this, tr("Select Folder Name"), tr("Enter folder name:")));
215 
216 	if (!title.isEmpty())
217 	{
218 		m_ui->feedsViewWidget->setCurrentIndex(FeedsManager::getModel()->addEntry(FeedsModel::FolderEntry, {{FeedsModel::TitleRole, title}}, findFolder(m_ui->feedsViewWidget->currentIndex()))->index());
219 	}
220 }
221 
openFeed()222 void FeedsContentsWidget::openFeed()
223 {
224 	const FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
225 	MainWindow *mainWindow(MainWindow::findMainWindow(this));
226 
227 	if (mainWindow && entry && entry->getFeed())
228 	{
229 		mainWindow->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), QUrl(QLatin1String("view-feed:") + entry->getFeed()->getUrl().toDisplayString())}});
230 	}
231 }
232 
updateFeed()233 void FeedsContentsWidget::updateFeed()
234 {
235 	const FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
236 
237 	if (entry && entry->getFeed())
238 	{
239 		entry->getFeed()->update();
240 	}
241 }
242 
removeFeed()243 void FeedsContentsWidget::removeFeed()
244 {
245 	FeedsManager::getModel()->trashEntry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
246 }
247 
subscribeFeed()248 void FeedsContentsWidget::subscribeFeed()
249 {
250 	if (!m_feed)
251 	{
252 		return;
253 	}
254 
255 	if (m_ui->applicationComboBox->currentIndex() == 0)
256 	{
257 		FeedPropertiesDialog dialog(m_feed, findFolder(m_ui->feedsViewWidget->currentIndex()), this);
258 
259 		if (dialog.exec() == QDialog::Accepted)
260 		{
261 			FeedsManager::getModel()->addEntry(m_feed, dialog.getFolder());
262 		}
263 	}
264 	else
265 	{
266 		Utils::runApplication(m_ui->applicationComboBox->currentData(Qt::UserRole).toString(), m_feed->getUrl());
267 	}
268 
269 	m_ui->subscribeFeedWidget->hide();
270 }
271 
feedProperties()272 void FeedsContentsWidget::feedProperties()
273 {
274 	FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
275 
276 	if (entry)
277 	{
278 		FeedsModel::Entry *folder(findFolder(m_ui->feedsViewWidget->currentIndex()));
279 		FeedPropertiesDialog dialog(entry->getFeed(), folder, this);
280 
281 		if (dialog.exec() == QDialog::Accepted && dialog.getFolder() != folder)
282 		{
283 			FeedsManager::getModel()->moveEntry(entry, dialog.getFolder());
284 		}
285 
286 		updateActions();
287 	}
288 }
289 
openEntry()290 void FeedsContentsWidget::openEntry()
291 {
292 	const QModelIndex index(m_ui->entriesViewWidget->currentIndex().sibling(m_ui->entriesViewWidget->currentIndex().row(), 0));
293 	MainWindow *mainWindow(MainWindow::findMainWindow(this));
294 
295 	if (mainWindow && index.isValid() && !index.data(UrlRole).isNull())
296 	{
297 		mainWindow->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), index.data(UrlRole)}});
298 	}
299 }
300 
removeEntry()301 void FeedsContentsWidget::removeEntry()
302 {
303 	if (m_feed)
304 	{
305 		const QModelIndex index(m_ui->entriesViewWidget->currentIndex().sibling(m_ui->entriesViewWidget->currentIndex().row(), 0));
306 
307 		if (index.isValid() && !index.data(IdentifierRole).isNull())
308 		{
309 			m_feed->markEntryAsRemoved(index.data(IdentifierRole).toString());
310 
311 			m_ui->entriesViewWidget->removeRow();
312 		}
313 	}
314 }
315 
selectCategory()316 void FeedsContentsWidget::selectCategory()
317 {
318 	const QToolButton *toolButton(qobject_cast<QToolButton*>(sender()));
319 
320 	if (toolButton)
321 	{
322 		m_categories = QStringList({toolButton->objectName()});
323 
324 		updateFeedModel();
325 	}
326 }
327 
handleFeedModified(const QUrl & url)328 void FeedsContentsWidget::handleFeedModified(const QUrl &url)
329 {
330 	const Feed *feed(FeedsManager::getFeed(url));
331 
332 	if (feed && feed->isUpdating() && FeedsManager::getModel()->hasFeed(url))
333 	{
334 		if (!m_updateAnimation)
335 		{
336 			const QString path(ThemesManager::getAnimationPath(QLatin1String("spinner")));
337 
338 			if (path.isEmpty())
339 			{
340 				m_updateAnimation = new SpinnerAnimation(QCoreApplication::instance());
341 			}
342 			else
343 			{
344 				m_updateAnimation = new GenericAnimation(path, QCoreApplication::instance());
345 			}
346 
347 			m_updateAnimation->start();
348 		}
349 
350 		connect(m_updateAnimation, &Animation::frameChanged, m_ui->feedsViewWidget->viewport(), static_cast<void(QWidget::*)()>(&QWidget::update), Qt::UniqueConnection);
351 	}
352 }
353 
showEntriesContextMenu(const QPoint & position)354 void FeedsContentsWidget::showEntriesContextMenu(const QPoint &position)
355 {
356 	const QModelIndex index(m_ui->entriesViewWidget->indexAt(position).sibling(m_ui->entriesViewWidget->indexAt(position).row(), 0));
357 	ActionExecutor::Object executor(this, this);
358 	QMenu menu(this);
359 	Action *openAction(new Action(ActionsManager::OpenUrlAction, {{QLatin1String("url"), index.data(UrlRole)}}, {{QLatin1String("text"), tr("Open")}}, executor, &menu));
360 	openAction->setEnabled(!index.data(UrlRole).isNull());
361 
362 	menu.addAction(openAction);
363 	menu.addSeparator();
364 	menu.addAction(new Action(ActionsManager::DeleteAction, {}, executor, &menu));
365 	menu.exec(m_ui->entriesViewWidget->mapToGlobal(position));
366 }
367 
showFeedsContextMenu(const QPoint & position)368 void FeedsContentsWidget::showFeedsContextMenu(const QPoint &position)
369 {
370 	const QModelIndex index(m_ui->feedsViewWidget->indexAt(position));
371 	const FeedsModel::EntryType type(static_cast<FeedsModel::EntryType>(index.data(FeedsModel::TypeRole).toInt()));
372 	QMenu menu(this);
373 
374 	switch (type)
375 	{
376 		case FeedsModel::TrashEntry:
377 			menu.addAction(ThemesManager::createIcon(QLatin1String("trash-empty")), tr("Empty Trash"), FeedsManager::getModel(), &FeedsModel::emptyTrash)->setEnabled(FeedsManager::getModel()->getTrashEntry()->rowCount() > 0);
378 
379 			break;
380 		case FeedsModel::UnknownEntry:
381 			menu.addAction(ThemesManager::createIcon(QLatin1String("inode-directory")), tr("Add Folder…"), this, &FeedsContentsWidget::addFolder);
382 			menu.addAction(tr("Add Feed…"), this, &FeedsContentsWidget::addFeed);
383 
384 			break;
385 		default:
386 			{
387 				const bool isInTrash(index.data(FeedsModel::IsTrashedRole).toBool());
388 
389 				ActionExecutor::Object executor(this, this);
390 
391 				if (!isInTrash)
392 				{
393 					if (type == FeedsModel::FeedEntry)
394 					{
395 						menu.addAction(ThemesManager::createIcon(QLatin1String("view-refresh")), QCoreApplication::translate("actions", "Update"), this, &FeedsContentsWidget::updateFeed);
396 						menu.addSeparator();
397 						menu.addAction(ThemesManager::createIcon(QLatin1String("document-open")), QCoreApplication::translate("actions", "Open"), this, &FeedsContentsWidget::openFeed);
398 					}
399 
400 					menu.addSeparator();
401 
402 					QMenu *addMenu(menu.addMenu(tr("Add New")));
403 					addMenu->addAction(ThemesManager::createIcon(QLatin1String("inode-directory")), tr("Add Folder…"), this, &FeedsContentsWidget::addFolder);
404 					addMenu->addAction(tr("Add Feed…"), this, &FeedsContentsWidget::addFeed);
405 				}
406 
407 				if (type != FeedsModel::RootEntry)
408 				{
409 					menu.addSeparator();
410 
411 					if (isInTrash)
412 					{
413 						menu.addAction(tr("Restore Feed"), &menu, [&]()
414 						{
415 							FeedsManager::getModel()->restoreEntry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
416 						});
417 					}
418 					else
419 					{
420 						menu.addAction(new Action(ActionsManager::DeleteAction, {}, executor, &menu));
421 					}
422 
423 					if (type == FeedsModel::FeedEntry)
424 					{
425 						menu.addSeparator();
426 						menu.addAction(tr("Properties…"), this, &FeedsContentsWidget::feedProperties);
427 					}
428 				}
429 			}
430 
431 			break;
432 	}
433 
434 	menu.exec(m_ui->feedsViewWidget->mapToGlobal(position));
435 }
436 
updateActions()437 void FeedsContentsWidget::updateActions()
438 {
439 	if (!isSidebarPanel())
440 	{
441 		const FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
442 
443 		setFeed((entry && entry->getFeed()) ? entry->getFeed() : nullptr);
444 	}
445 }
446 
updateEntry()447 void FeedsContentsWidget::updateEntry()
448 {
449 	const QModelIndex index(m_ui->entriesViewWidget->currentIndex().sibling(m_ui->entriesViewWidget->currentIndex().row(), 0));
450 	QString content(index.data(ContentRole).toString());
451 
452 	if (!index.data(SummaryRole).isNull())
453 	{
454 		QString summary(index.data(SummaryRole).toString());
455 
456 		if (!summary.contains(QLatin1Char('<')))
457 		{
458 			summary = QLatin1String("<p>") + summary + QLatin1String("</p>");
459 		}
460 
461 		summary.append(QLatin1Char('\n'));
462 
463 		content.prepend(summary);
464 	}
465 
466 	const QString enableImages(SettingsManager::getOption(SettingsManager::Permissions_EnableImagesOption, Utils::extractHost(m_feed->getUrl())).toString());
467 	TextBrowserWidget::ImagesPolicy imagesPolicy(TextBrowserWidget::AllImages);
468 
469 	if (enableImages == QLatin1String("onlyCached"))
470 	{
471 		imagesPolicy = TextBrowserWidget::OnlyCachedImages;
472 	}
473 	else if (enableImages == QLatin1String("disabled"))
474 	{
475 		imagesPolicy = TextBrowserWidget::NoImages;
476 	}
477 
478 	m_ui->titleLabelWidget->setText(index.isValid() ? index.data(Qt::DisplayRole).toString() : QString());
479 	m_ui->emailButton->setVisible(!index.data(EmailRole).isNull());
480 	m_ui->emailButton->setToolTip(tr("Send email to %1").arg(index.data(EmailRole).toString()));
481 	m_ui->urlButton->setVisible(!index.data(UrlRole).isNull());
482 	m_ui->urlButton->setToolTip(tr("Go to %1").arg(index.data(UrlRole).toUrl().toDisplayString()));
483 	m_ui->authorLabelWidget->setText(index.isValid() ? index.data(AuthorRole).toString() : QString());
484 	m_ui->timeLabelWidget->setText(index.isValid() ? Utils::formatDateTime(index.data(index.data(UpdateTimeRole).isNull() ? PublicationTimeRole : UpdateTimeRole).toDateTime()) : QString());
485 	m_ui->textBrowserWidget->setImagesPolicy(imagesPolicy);
486 	m_ui->textBrowserWidget->setText(content);
487 
488 	for (int i = (m_ui->categoriesLayout->count() - 1); i >= 0; --i)
489 	{
490 		m_ui->categoriesLayout->takeAt(i)->widget()->deleteLater();
491 	}
492 
493 	const QStringList entryCategories(index.data(CategoriesRole).toStringList());
494 	const QMap<QString, QString> feedCategories(m_feed ? m_feed->getCategories() : QMap<QString, QString>());
495 
496 	for (int i = 0; i < entryCategories.count(); ++i)
497 	{
498 		if (!entryCategories.at(i).isEmpty())
499 		{
500 			const QString label(feedCategories.value(entryCategories.at(i)));
501 			QToolButton *toolButton(new QToolButton(m_ui->entryWidget));
502 			toolButton->setText(label.isEmpty() ? QString(entryCategories.at(i)).replace(QLatin1Char('_'), QLatin1Char(' ')) : label);
503 			toolButton->setObjectName(entryCategories.at(i));
504 
505 			m_ui->categoriesLayout->addWidget(toolButton);
506 
507 			connect(toolButton, &QToolButton::clicked, this, &FeedsContentsWidget::selectCategory);
508 		}
509 	}
510 
511 	m_ui->categoriesLayout->addStretch();
512 
513 	if (index.isValid() && m_feed && m_feedModel)
514 	{
515 		disconnect(m_ui->entriesViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateEntry);
516 
517 		m_feed->markEntryAsRead(index.data(IdentifierRole).toString());
518 
519 		m_feedModel->setData(index, QDateTime::currentDateTimeUtc(), LastReadTimeRole);
520 
521 		m_ui->entriesViewWidget->update();
522 
523 		connect(m_ui->entriesViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateEntry);
524 	}
525 
526 	emit arbitraryActionsStateChanged({ActionsManager::DeleteAction});
527 }
528 
updateFeedModel()529 void FeedsContentsWidget::updateFeedModel()
530 {
531 	if (!m_feedModel)
532 	{
533 		return;
534 	}
535 
536 	const QString identifier(m_ui->entriesViewWidget->currentIndex().data(IdentifierRole).toString());
537 
538 	m_feedModel->clear();
539 	m_feedModel->setHorizontalHeaderLabels({tr("Title"), tr("From"), tr("Published")});
540 	m_feedModel->setHeaderData(0, Qt::Horizontal, 300, HeaderViewWidget::WidthRole);
541 
542 	m_ui->applicationComboBox->clear();
543 
544 	if (m_feed && !FeedsManager::getModel()->hasFeed(m_feed->getUrl()))
545 	{
546 		m_ui->applicationComboBox->setItemDelegate(new ItemDelegate(m_ui->applicationComboBox));
547 		m_ui->applicationComboBox->addItem(Application::windowIcon(), Application::applicationDisplayName());
548 
549 		const QVector<ApplicationInformation> applications(Utils::getApplicationsForMimeType(m_feed->getMimeType()));
550 
551 		if (applications.count() > 0)
552 		{
553 			m_ui->applicationComboBox->addItem({});
554 			m_ui->applicationComboBox->setItemData(1, QLatin1String("separator"), Qt::AccessibleDescriptionRole);
555 
556 			for (int i = 0; i < applications.count(); ++i)
557 			{
558 				m_ui->applicationComboBox->addItem(applications.at(i).icon, applications.at(i).name);
559 				m_ui->applicationComboBox->setItemData((i + 2), applications.at(i).command, Qt::UserRole);
560 
561 				if (applications.at(i).icon.isNull())
562 				{
563 					m_ui->applicationComboBox->setItemData((i + 2), QColor(Qt::transparent), Qt::DecorationRole);
564 				}
565 			}
566 		}
567 	}
568 
569 	if (m_ui->categoriesButton->menu())
570 	{
571 		m_ui->categoriesButton->menu()->deleteLater();
572 	}
573 
574 	QMenu *menu(new QMenu(m_ui->categoriesButton));
575 
576 	m_ui->categoriesButton->setMenu(menu);
577 
578 	connect(menu, &QMenu::triggered, [=](QAction *action)
579 	{
580 		if (action->data().isNull() && action->isChecked())
581 		{
582 			m_categories.clear();
583 		}
584 		else if (menu->actions().count() > 0)
585 		{
586 			QStringList categories;
587 			bool hasAllCategories = true;
588 
589 			m_categories.clear();
590 
591 			for (int i = 2; i < menu->actions().count(); ++i)
592 			{
593 				if (menu->actions().at(i)->isChecked())
594 				{
595 					categories.append(menu->actions().at(i)->data().toString());
596 				}
597 				else
598 				{
599 					hasAllCategories = false;
600 				}
601 			}
602 
603 			menu->actions().first()->setChecked(hasAllCategories);
604 
605 			if (!hasAllCategories)
606 			{
607 				m_categories = categories;
608 			}
609 		}
610 
611 		updateFeedModel();
612 	});
613 
614 	if (!m_feed)
615 	{
616 		return;
617 	}
618 
619 	QAction *allAction(menu->addAction(tr("All (%1)").arg(m_feed->getEntries().count())));
620 	allAction->setCheckable(true);
621 	allAction->setChecked(m_categories.isEmpty());
622 
623 	menu->addSeparator();
624 
625 	const QMap<QString, QString> categories(m_feed->getCategories());
626 	QMap<QString, QString>::const_iterator iterator;
627 
628 	for (iterator = categories.begin(); iterator != categories.end(); ++iterator)
629 	{
630 		const int amount(m_feed->getEntries({iterator.key()}).count());
631 
632 		if (amount > 0)
633 		{
634 			QAction *action(menu->addAction((iterator.value().isEmpty() ? QString(iterator.key()).replace(QLatin1Char('_'), QLatin1Char(' ')) : iterator.value()) + QLatin1String(" (") + QString::number(amount) + QLatin1Char(')')));
635 			action->setCheckable(true);
636 			action->setChecked(m_categories.isEmpty() || m_categories.contains(iterator.key()));
637 			action->setData(iterator.key());
638 		}
639 	}
640 
641 	m_ui->categoriesButton->setEnabled(!categories.isEmpty());
642 
643 	const QVector<Feed::Entry> entries(m_feed->getEntries(m_categories));
644 
645 	for (int i = 0; i < entries.count(); ++i)
646 	{
647 		const Feed::Entry entry(entries.at(i));
648 
649 		if (!m_categories.isEmpty())
650 		{
651 			bool hasFound(false);
652 
653 			for (int j = 0; j < m_categories.count(); ++j)
654 			{
655 				if (entry.categories.contains(m_categories.at(j)))
656 				{
657 					hasFound = true;
658 
659 					break;
660 				}
661 			}
662 
663 			if (!hasFound)
664 			{
665 				continue;
666 			}
667 		}
668 
669 		QList<QStandardItem*> items({new QStandardItem(entry.title.isEmpty() ? tr("(Untitled)") : entry.title), new QStandardItem(entry.author.isEmpty() ? tr("(Untitled)") : entry.author), new QStandardItem(Utils::formatDateTime(entry.updateTime.isNull() ? entry.publicationTime : entry.updateTime))});
670 		items[0]->setData(entry.url, UrlRole);
671 		items[0]->setData(entry.identifier, IdentifierRole);
672 		items[0]->setData(entry.summary, SummaryRole);
673 		items[0]->setData(entry.content, ContentRole);
674 		items[0]->setData(entry.publicationTime, PublicationTimeRole);
675 		items[0]->setData(entry.updateTime, UpdateTimeRole);
676 		items[0]->setData(entry.author, AuthorRole);
677 		items[0]->setData(entry.email, EmailRole);
678 		items[0]->setData(entry.lastReadTime, LastReadTimeRole);
679 		items[0]->setData(entry.categories, CategoriesRole);
680 		items[0]->setFlags(items[0]->flags() | Qt::ItemNeverHasChildren);
681 		items[1]->setFlags(items[1]->flags() | Qt::ItemNeverHasChildren);
682 		items[2]->setFlags(items[2]->flags() | Qt::ItemNeverHasChildren);
683 
684 		m_feedModel->appendRow(items);
685 
686 		if (entry.identifier == identifier)
687 		{
688 			m_ui->entriesViewWidget->setCurrentIndex(m_ui->entriesViewWidget->getIndex(i));
689 		}
690 	}
691 
692 	if (!m_ui->entriesViewWidget->selectionModel()->hasSelection())
693 	{
694 		m_ui->entriesViewWidget->setCurrentIndex(m_ui->entriesViewWidget->getIndex(0));
695 	}
696 }
697 
setFeed(Feed * feed)698 void FeedsContentsWidget::setFeed(Feed *feed)
699 {
700 	if (feed == m_feed)
701 	{
702 		return;
703 	}
704 
705 	if (m_feed)
706 	{
707 		disconnect(m_feed, &Feed::entriesModified, this, &FeedsContentsWidget::updateFeedModel);
708 	}
709 
710 	m_feed = feed;
711 
712 	if (m_feedModel)
713 	{
714 		m_feedModel->clear();
715 	}
716 
717 	if (m_feed)
718 	{
719 		m_categories.clear();
720 
721 		if (!m_feedModel)
722 		{
723 			m_feedModel = new QStandardItemModel(this);
724 
725 			m_ui->entriesViewWidget->setModel(m_feedModel);
726 			m_ui->entriesViewWidget->setViewMode(ItemViewWidget::ListView);
727 		}
728 
729 		if (m_feed->getLastSynchronizationTime().isNull())
730 		{
731 			m_feed->update();
732 		}
733 
734 		if (!FeedsManager::getModel()->hasFeed(m_feed->getUrl()))
735 		{
736 			m_ui->iconLabel->setPixmap(ThemesManager::createIcon(QLatin1String("application-rss+xml"), false).pixmap(m_ui->iconLabel->size()));
737 			m_ui->messageLabel->setText(tr("Subscribe to this feed using:"));
738 
739 			m_ui->subscribeFeedWidget->show();
740 		}
741 		else
742 		{
743 			m_ui->subscribeFeedWidget->hide();
744 		}
745 
746 		connect(m_feed, &Feed::entriesModified, this, &FeedsContentsWidget::updateFeedModel);
747 	}
748 
749 	updateFeedModel();
750 
751 	emit arbitraryActionsStateChanged({ActionsManager::ReloadAction});
752 	emit titleChanged(getTitle());
753 	emit iconChanged(getIcon());
754 	emit urlChanged(getUrl());
755 }
756 
setUrl(const QUrl & url,bool isTyped)757 void FeedsContentsWidget::setUrl(const QUrl &url, bool isTyped)
758 {
759 	Q_UNUSED(isTyped)
760 
761 	if (url.scheme() == QLatin1String("view-feed"))
762 	{
763 		setFeed(FeedsManager::createFeed(url.toDisplayString().mid(10)));
764 	}
765 }
766 
getUpdateAnimation()767 Animation* FeedsContentsWidget::getUpdateAnimation()
768 {
769 	return m_updateAnimation;
770 }
771 
findFolder(const QModelIndex & index) const772 FeedsModel::Entry* FeedsContentsWidget::findFolder(const QModelIndex &index) const
773 {
774 	FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(index));
775 
776 	if (!entry || entry == FeedsManager::getModel()->getRootEntry() || entry == FeedsManager::getModel()->getTrashEntry())
777 	{
778 		return FeedsManager::getModel()->getRootEntry();
779 	}
780 
781 	const FeedsModel::EntryType type(static_cast<FeedsModel::EntryType>(entry->data(FeedsModel::TypeRole).toInt()));
782 
783 	return ((type == FeedsModel::RootEntry || type == FeedsModel::FolderEntry) ? entry : static_cast<FeedsModel::Entry*>(entry->parent()));
784 }
785 
getTitle() const786 QString FeedsContentsWidget::getTitle() const
787 {
788 	if (m_feed)
789 	{
790 		const QString title(m_feed->getTitle());
791 
792 		return tr("Feed: %1").arg(title.isEmpty() ? tr("(Untitled)") : title);
793 	}
794 
795 	return tr("Feeds");
796 }
797 
getType() const798 QLatin1String FeedsContentsWidget::getType() const
799 {
800 	return QLatin1String("feeds");
801 }
802 
getUrl() const803 QUrl FeedsContentsWidget::getUrl() const
804 {
805 	return (m_feed ? QUrl(QLatin1String("view-feed:") + m_feed->getUrl().toDisplayString()) : QUrl(QLatin1String("about:feeds")));
806 }
807 
getIcon() const808 QIcon FeedsContentsWidget::getIcon() const
809 {
810 	if (m_feed)
811 	{
812 		const QIcon icon(m_feed->getIcon());
813 
814 		return (icon.isNull() ? ThemesManager::createIcon(QLatin1String("application-rss+xml")) : icon);
815 	}
816 
817 	return ThemesManager::createIcon(QLatin1String("feeds"), false);
818 }
819 
getActionState(int identifier,const QVariantMap & parameters) const820 ActionsManager::ActionDefinition::State FeedsContentsWidget::getActionState(int identifier, const QVariantMap &parameters) const
821 {
822 	ActionsManager::ActionDefinition::State state(ActionsManager::getActionDefinition(identifier).getDefaultState());
823 
824 	switch (identifier)
825 	{
826 		case ActionsManager::ReloadAction:
827 			state.isEnabled = (m_feed != nullptr);
828 
829 			return state;
830 		case ActionsManager::DeleteAction:
831 			state.isEnabled = false;
832 
833 			if (m_ui->feedsViewWidget->hasFocus())
834 			{
835 				const FeedsModel::EntryType type(static_cast<FeedsModel::EntryType>(m_ui->feedsViewWidget->currentIndex().data(FeedsModel::TypeRole).toInt()));
836 
837 				state.isEnabled = (type == FeedsModel::FeedEntry || type == FeedsModel::FolderEntry);
838 			}
839 			else if (m_ui->entriesViewWidget->hasFocus())
840 			{
841 				state.isEnabled = (m_feed && m_ui->entriesViewWidget->selectionModel()->hasSelection());
842 			}
843 
844 			return state;
845 		default:
846 			break;
847 	}
848 
849 	return ContentsWidget::getActionState(identifier, parameters);
850 }
851 
eventFilter(QObject * object,QEvent * event)852 bool FeedsContentsWidget::eventFilter(QObject *object, QEvent *event)
853 {
854 	if ((object == m_ui->entriesViewWidget || object == m_ui->feedsViewWidget ) && event->type() == QEvent::KeyPress)
855 	{
856 		switch (static_cast<QKeyEvent*>(event)->key())
857 		{
858 			case Qt::Key_Delete:
859 				if (m_ui->feedsViewWidget->hasFocus())
860 				{
861 					removeFeed();
862 				}
863 				else if (m_ui->entriesViewWidget->hasFocus())
864 				{
865 					removeEntry();
866 				}
867 
868 				return true;
869 			case Qt::Key_Enter:
870 			case Qt::Key_Return:
871 				if (m_ui->feedsViewWidget->hasFocus())
872 				{
873 					openFeed();
874 				}
875 				else if (m_ui->entriesViewWidget->hasFocus())
876 				{
877 					openEntry();
878 				}
879 
880 				return true;
881 			default:
882 				break;
883 		}
884 	}
885 	else if ((object == m_ui->entriesViewWidget->viewport() || object == m_ui->feedsViewWidget->viewport()) && event->type() == QEvent::ToolTip)
886 	{
887 		const QHelpEvent *helpEvent(static_cast<QHelpEvent*>(event));
888 		ItemViewWidget *viewWidget(object == m_ui->feedsViewWidget->viewport() ? m_ui->feedsViewWidget : m_ui->entriesViewWidget);
889 		const QModelIndex index(viewWidget->indexAt(helpEvent->pos()));
890 
891 		if (static_cast<FeedsModel::EntryType>(index.data(FeedsModel::TypeRole).toInt()) != FeedsModel::FeedEntry)
892 		{
893 			return false;
894 		}
895 
896 		QString toolTip;
897 
898 		if (index.isValid())
899 		{
900 			if (object == m_ui->feedsViewWidget)
901 			{
902 				toolTip = tr("Title: %1").arg(index.data(FeedsModel::TitleRole).toString()) + QLatin1Char('\n') + tr("Address: %1").arg(index.data(FeedsModel::UrlRole).toUrl().toDisplayString());
903 
904 				if (!index.data(FeedsModel::LastSynchronizationTimeRole).isNull())
905 				{
906 					toolTip.append(QLatin1Char('\n') + tr("Last update: %1").arg(Utils::formatDateTime(index.data(FeedsModel::LastSynchronizationTimeRole).toDateTime())));
907 				}
908 			}
909 			else
910 			{
911 				toolTip = tr("Title: %1").arg(index.data(TitleRole).toString());
912 
913 				if (!index.data(UrlRole).isNull())
914 				{
915 					toolTip.append(QLatin1Char('\n') + tr("Address: %1").arg(index.data(UrlRole).toUrl().toDisplayString()));
916 				}
917 			}
918 		}
919 
920 		QToolTip::showText(helpEvent->globalPos(), QFontMetrics(QToolTip::font()).elidedText(toolTip, Qt::ElideRight, (QApplication::desktop()->screenGeometry(viewWidget).width() / 2)), viewWidget, viewWidget->visualRect(index));
921 
922 		return true;
923 	}
924 
925 	return ContentsWidget::eventFilter(object, event);
926 }
927 
928 }
929