1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2018 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 "FeedsModel.h"
21 #include "Application.h"
22 #include "Console.h"
23 #include "FeedsManager.h"
24 #include "SessionsManager.h"
25 #include "ThemesManager.h"
26 #include "Utils.h"
27 
28 #include <QtCore/QCoreApplication>
29 #include <QtCore/QFile>
30 #include <QtCore/QSaveFile>
31 #include <QtCore/QXmlStreamReader>
32 #include <QtCore/QXmlStreamWriter>
33 #include <QtWidgets/QMessageBox>
34 
35 namespace Otter
36 {
37 
Entry(Feed * feed)38 FeedsModel::Entry::Entry(Feed *feed) : QStandardItem(),
39 	m_feed(feed)
40 {
41 }
42 
getFeed() const43 Feed* FeedsModel::Entry::getFeed() const
44 {
45 	return m_feed;
46 }
47 
data(int role) const48 QVariant FeedsModel::Entry::data(int role) const
49 {
50 	if (role == TitleRole)
51 	{
52 		switch (getType())
53 		{
54 			case RootEntry:
55 				return QCoreApplication::translate("Otter::FeedsModel", "Feeds");
56 			case TrashEntry:
57 				return QCoreApplication::translate("Otter::FeedsModel", "Trash");
58 			case FolderEntry:
59 				if (QStandardItem::data(role).isNull())
60 				{
61 					return QCoreApplication::translate("Otter::FeedsModel", "(Untitled)");
62 				}
63 
64 				break;
65 			case FeedEntry:
66 				if (m_feed && !m_feed->getTitle().isEmpty())
67 				{
68 					return m_feed->getTitle();
69 				}
70 
71 				break;
72 			default:
73 				break;
74 		}
75 	}
76 
77 	if (role == Qt::DecorationRole)
78 	{
79 		switch (getType())
80 		{
81 			case RootEntry:
82 			case FolderEntry:
83 				return ThemesManager::createIcon(QLatin1String("inode-directory"));
84 			case TrashEntry:
85 				return ThemesManager::createIcon(QLatin1String("user-trash"));
86 			case FeedEntry:
87 				if (m_feed)
88 				{
89 					if (m_feed->getError() != Feed::NoError)
90 					{
91 						return ThemesManager::createIcon(QLatin1String("dialog-error"));
92 					}
93 
94 					if (!m_feed->getIcon().isNull())
95 					{
96 						return m_feed->getIcon();
97 					}
98 				}
99 
100 				return ThemesManager::createIcon(QLatin1String("application-rss+xml"));
101 			default:
102 				break;
103 		}
104 
105 		return {};
106 	}
107 
108 	if (m_feed)
109 	{
110 		switch (role)
111 		{
112 			case LastUpdateTimeRole:
113 				return m_feed->getLastUpdateTime();
114 			case LastSynchronizationTimeRole:
115 				return m_feed->getLastSynchronizationTime();
116 			case UrlRole:
117 				return m_feed->getUrl();
118 			case UpdateIntervalRole:
119 				return m_feed->getUpdateInterval();
120 			case IsUpdatingRole:
121 				return m_feed->isUpdating();
122 			case HasErrorsRole:
123 				return (m_feed->getError() != Feed::NoError);
124 			default:
125 				break;
126 		}
127 	}
128 
129 	if (role == IsTrashedRole)
130 	{
131 		QModelIndex parent(index().parent());
132 
133 		while (parent.isValid())
134 		{
135 			const EntryType type(static_cast<EntryType>(parent.data(TypeRole).toInt()));
136 
137 			if (type == RootEntry)
138 			{
139 				break;
140 			}
141 
142 			if (type == TrashEntry)
143 			{
144 				return true;
145 			}
146 
147 			parent = parent.parent();
148 		}
149 
150 		return false;
151 	}
152 
153 	return QStandardItem::data(role);
154 }
155 
getRawData(int role) const156 QVariant FeedsModel::Entry::getRawData(int role) const
157 {
158 	if (m_feed)
159 	{
160 		switch (role)
161 		{
162 			case Qt::DecorationRole:
163 				return m_feed->getIcon();
164 			case TitleRole:
165 				return m_feed->getTitle();
166 			case LastUpdateTimeRole:
167 				return m_feed->getLastUpdateTime();
168 			case LastSynchronizationTimeRole:
169 				return m_feed->getLastSynchronizationTime();
170 			case UrlRole:
171 				return m_feed->getUrl();
172 			case UpdateIntervalRole:
173 				return m_feed->getUpdateInterval();
174 			default:
175 				break;
176 		}
177 	}
178 
179 	return QStandardItem::data(role);
180 }
181 
getFeeds() const182 QVector<Feed*> FeedsModel::Entry::getFeeds() const
183 {
184 	QVector<Feed*> feeds;
185 
186 	if (getType() == FeedEntry)
187 	{
188 		feeds.append(m_feed);
189 	}
190 
191 	for (int i = 0; i < rowCount(); ++i)
192 	{
193 		const Entry *entry(static_cast<Entry*>(child(i, 0)));
194 
195 		if (!entry)
196 		{
197 			continue;
198 		}
199 
200 		switch (entry->getType())
201 		{
202 			case FeedEntry:
203 				feeds.append(entry->getFeed());
204 
205 				break;
206 			case FolderEntry:
207 				feeds.append(entry->getFeeds());
208 
209 				break;
210 			default:
211 				break;
212 		}
213 	}
214 
215 	return feeds;
216 }
217 
getType() const218 FeedsModel::EntryType FeedsModel::Entry::getType() const
219 {
220 	return static_cast<EntryType>(data(TypeRole).toInt());
221 }
222 
isAncestorOf(FeedsModel::Entry * child) const223 bool FeedsModel::Entry::isAncestorOf(FeedsModel::Entry *child) const
224 {
225 	if (child == nullptr || child == this)
226 	{
227 		return false;
228 	}
229 
230 	QStandardItem *parent(child->parent());
231 
232 	while (parent)
233 	{
234 		if (parent == this)
235 		{
236 			return true;
237 		}
238 
239 		parent = parent->parent();
240 	}
241 
242 	return false;
243 }
244 
operator <(const QStandardItem & other) const245 bool FeedsModel::Entry::operator<(const QStandardItem &other) const
246 {
247 	const EntryType type(getType());
248 
249 	if (type == RootEntry || type == TrashEntry)
250 	{
251 		return false;
252 	}
253 
254 	return QStandardItem::operator<(other);
255 }
256 
FeedsModel(const QString & path,QObject * parent)257 FeedsModel::FeedsModel(const QString &path, QObject *parent) : QStandardItemModel(parent),
258 	m_rootEntry(new Entry()),
259 	m_trashEntry(new Entry()),
260 	m_importTargetEntry(nullptr)
261 {
262 	m_rootEntry->setData(RootEntry, TypeRole);
263 	m_rootEntry->setDragEnabled(false);
264 	m_trashEntry->setData(TrashEntry, TypeRole);
265 	m_trashEntry->setDragEnabled(false);
266 	m_trashEntry->setEnabled(false);
267 
268 	appendRow(m_rootEntry);
269 	appendRow(m_trashEntry);
270 	setItemPrototype(new Entry());
271 
272 	if (!QFile::exists(path))
273 	{
274 		return;
275 	}
276 
277 	QFile file(path);
278 
279 	if (!file.open(QIODevice::ReadOnly))
280 	{
281 		Console::addMessage(tr("Failed to open feeds file: %1").arg(file.errorString()), Console::OtherCategory, Console::ErrorLevel, path);
282 
283 		return;
284 	}
285 
286 	QXmlStreamReader reader(&file);
287 
288 	if (reader.readNextStartElement() && reader.name() == QLatin1String("opml") && reader.attributes().value(QLatin1String("version")).toString() == QLatin1String("1.0"))
289 	{
290 		while (reader.readNextStartElement())
291 		{
292 			if (reader.name() == QLatin1String("outline"))
293 			{
294 				readEntry(&reader, m_rootEntry);
295 			}
296 
297 			if (reader.name() != QLatin1String("body"))
298 			{
299 				reader.skipCurrentElement();
300 			}
301 
302 			if (reader.hasError() && rowCount() == 0)
303 			{
304 				Console::addMessage(tr("Failed to load feeds file: %1").arg(reader.errorString()), Console::OtherCategory, Console::ErrorLevel, file.fileName());
305 
306 				QMessageBox::warning(nullptr, tr("Error"), tr("Failed to load feeds file."), QMessageBox::Close);
307 
308 				return;
309 			}
310 		}
311 	}
312 
313 	file.close();
314 }
315 
beginImport(Entry * target,int estimatedUrlsAmount)316 void FeedsModel::beginImport(Entry *target, int estimatedUrlsAmount)
317 {
318 	m_importTargetEntry = target;
319 
320 	beginResetModel();
321 	blockSignals(true);
322 
323 	if (estimatedUrlsAmount > 0)
324 	{
325 		m_urls.reserve(m_urls.count() + estimatedUrlsAmount);
326 	}
327 }
328 
endImport()329 void FeedsModel::endImport()
330 {
331 	m_urls.squeeze();
332 
333 	blockSignals(false);
334 	endResetModel();
335 
336 	if (m_importTargetEntry)
337 	{
338 		emit entryModified(m_importTargetEntry);
339 
340 		m_importTargetEntry = nullptr;
341 	}
342 
343 	emit modelModified();
344 }
345 
trashEntry(Entry * entry)346 void FeedsModel::trashEntry(Entry *entry)
347 {
348 	if (!entry)
349 	{
350 		return;
351 	}
352 
353 	const EntryType type(entry->getType());
354 
355 	if (type != RootEntry && type != TrashEntry)
356 	{
357 		if (entry->data(IsTrashedRole).toBool())
358 		{
359 			removeEntry(entry);
360 		}
361 		else
362 		{
363 			Entry *previousParent(static_cast<Entry*>(entry->parent()));
364 
365 			m_trash[entry] = {entry->parent()->index(), entry->row()};
366 
367 			m_trashEntry->appendRow(entry->parent()->takeRow(entry->row()));
368 			m_trashEntry->setEnabled(true);
369 
370 			removeEntryUrl(entry);
371 
372 			emit entryModified(entry);
373 			emit entryTrashed(entry, previousParent);
374 			emit modelModified();
375 		}
376 	}
377 }
378 
restoreEntry(Entry * entry)379 void FeedsModel::restoreEntry(Entry *entry)
380 {
381 	if (!entry)
382 	{
383 		return;
384 	}
385 
386 	Entry *formerParent(m_trash.contains(entry) ? getEntry(m_trash[entry].first) : m_rootEntry);
387 
388 	if (!formerParent || formerParent->getType() != FolderEntry)
389 	{
390 		formerParent = m_rootEntry;
391 	}
392 
393 	if (m_trash.contains(entry))
394 	{
395 		formerParent->insertRow(m_trash[entry].second, entry->parent()->takeRow(entry->row()));
396 
397 		m_trash.remove(entry);
398 	}
399 	else
400 	{
401 		formerParent->appendRow(entry->parent()->takeRow(entry->row()));
402 	}
403 
404 	readdEntryUrl(entry);
405 
406 	m_trashEntry->setEnabled(m_trashEntry->rowCount() > 0);
407 
408 	emit entryModified(entry);
409 	emit entryRestored(entry);
410 	emit modelModified();
411 }
412 
removeEntry(Entry * entry)413 void FeedsModel::removeEntry(Entry *entry)
414 {
415 	if (!entry)
416 	{
417 		return;
418 	}
419 
420 	removeEntryUrl(entry);
421 
422 	const quint64 identifier(entry->data(IdentifierRole).toULongLong());
423 
424 	if (identifier > 0 && m_identifiers.contains(identifier))
425 	{
426 		m_identifiers.remove(identifier);
427 	}
428 
429 	emit entryRemoved(entry, static_cast<Entry*>(entry->parent()));
430 
431 	entry->parent()->removeRow(entry->row());
432 
433 	emit modelModified();
434 }
435 
readEntry(QXmlStreamReader * reader,Entry * parent)436 void FeedsModel::readEntry(QXmlStreamReader *reader, Entry *parent)
437 {
438 	const QString title(reader->attributes().value(reader->attributes().hasAttribute(QLatin1String("title")) ? QLatin1String("title") : QLatin1String("text")).toString());
439 
440 	if (reader->attributes().hasAttribute(QLatin1String("xmlUrl")))
441 	{
442 		const QUrl url(Utils::normalizeUrl(QUrl(reader->attributes().value(QLatin1String("xmlUrl")).toString())));
443 
444 		if (url.isValid())
445 		{
446 			Entry *entry(new Entry(FeedsManager::createFeed(url, title, Utils::loadPixmapFromDataUri(reader->attributes().value(QLatin1String("icon")).toString()), reader->attributes().value(QLatin1String("updateInterval")).toInt())));
447 			entry->setData(FeedEntry, TypeRole);
448 			entry->setFlags(entry->flags() | Qt::ItemNeverHasChildren);
449 
450 			createIdentifier(entry);
451 			handleUrlChanged(entry, url);
452 
453 			parent->appendRow(entry);
454 		}
455 	}
456 	else
457 	{
458 		Entry *entry(new Entry());
459 		entry->setData(FolderEntry, TypeRole);
460 		entry->setData(title, TitleRole);
461 
462 		createIdentifier(entry);
463 
464 		parent->appendRow(entry);
465 
466 		while (reader->readNext())
467 		{
468 			if (reader->isStartElement())
469 			{
470 				if (reader->name() == QLatin1String("outline"))
471 				{
472 					readEntry(reader, entry);
473 				}
474 				else
475 				{
476 					reader->skipCurrentElement();
477 				}
478 			}
479 			else if (reader->hasError())
480 			{
481 				return;
482 			}
483 		}
484 	}
485 }
486 
writeEntry(QXmlStreamWriter * writer,Entry * entry) const487 void FeedsModel::writeEntry(QXmlStreamWriter *writer, Entry *entry) const
488 {
489 	if (!entry)
490 	{
491 		return;
492 	}
493 
494 	writer->writeStartElement(QLatin1String("outline"));
495 	writer->writeAttribute(QLatin1String("text"), entry->getRawData(TitleRole).toString());
496 	writer->writeAttribute(QLatin1String("title"), entry->getRawData(TitleRole).toString());
497 
498 	switch (entry->getType())
499 	{
500 		case FolderEntry:
501 			for (int i = 0; i < entry->rowCount(); ++i)
502 			{
503 				writeEntry(writer, static_cast<Entry*>(entry->child(i, 0)));
504 			}
505 
506 			break;
507 		case FeedEntry:
508 			writer->writeAttribute(QLatin1String("xmlUrl"), entry->getRawData(UrlRole).toUrl().toString());
509 			writer->writeAttribute(QLatin1String("updateInterval"), QString::number(entry->getRawData(UpdateIntervalRole).toInt()));
510 
511 			if (!entry->getRawData(Qt::DecorationRole).isNull())
512 			{
513 				writer->writeAttribute(QLatin1String("icon"), Utils::savePixmapAsDataUri(entry->icon().pixmap(entry->icon().availableSizes().value(0, QSize(16, 16)))));
514 			}
515 
516 			break;
517 		default:
518 			break;
519 	}
520 
521 	writer->writeEndElement();
522 }
523 
removeEntryUrl(Entry * entry)524 void FeedsModel::removeEntryUrl(Entry *entry)
525 {
526 	if (!entry)
527 	{
528 		return;
529 	}
530 
531 	switch (entry->getType())
532 	{
533 		case FeedEntry:
534 			{
535 				const QUrl url(Utils::normalizeUrl(entry->data(UrlRole).toUrl()));
536 
537 				if (!url.isEmpty() && m_urls.contains(url))
538 				{
539 					m_urls[url].removeAll(entry);
540 
541 					if (m_urls[url].isEmpty())
542 					{
543 						m_urls.remove(url);
544 					}
545 				}
546 			}
547 
548 			break;
549 		case FolderEntry:
550 			for (int i = 0; i < entry->rowCount(); ++i)
551 			{
552 				removeEntryUrl(static_cast<Entry*>(entry->child(i, 0)));
553 			}
554 
555 			break;
556 		default:
557 			break;
558 	}
559 }
560 
readdEntryUrl(Entry * entry)561 void FeedsModel::readdEntryUrl(Entry *entry)
562 {
563 	if (!entry)
564 	{
565 		return;
566 	}
567 
568 	switch (entry->getType())
569 	{
570 		case FeedEntry:
571 			{
572 				const QUrl url(Utils::normalizeUrl(entry->data(UrlRole).toUrl()));
573 
574 				if (!url.isEmpty())
575 				{
576 					if (!m_urls.contains(url))
577 					{
578 						m_urls[url] = QVector<Entry*>();
579 					}
580 
581 					m_urls[url].append(entry);
582 				}
583 			}
584 
585 			break;
586 		case FolderEntry:
587 			for (int i = 0; i < entry->rowCount(); ++i)
588 			{
589 				readdEntryUrl(static_cast<Entry*>(entry->child(i, 0)));
590 			}
591 
592 			break;
593 		default:
594 			break;
595 	}
596 }
597 
emptyTrash()598 void FeedsModel::emptyTrash()
599 {
600 	m_trashEntry->removeRows(0, m_trashEntry->rowCount());
601 	m_trashEntry->setEnabled(false);
602 
603 	m_trash.clear();
604 
605 	emit modelModified();
606 }
607 
createIdentifier(FeedsModel::Entry * entry)608 void FeedsModel::createIdentifier(FeedsModel::Entry *entry)
609 {
610 	const quint64 identifier(m_identifiers.isEmpty() ? 1 : (m_identifiers.keys().last() + 1));
611 
612 	m_identifiers[identifier] = entry;
613 
614 	entry->setData(identifier, IdentifierRole);
615 }
616 
handleUrlChanged(Entry * entry,const QUrl & newUrl,const QUrl & oldUrl)617 void FeedsModel::handleUrlChanged(Entry *entry, const QUrl &newUrl, const QUrl &oldUrl)
618 {
619 	if (!oldUrl.isEmpty() && m_urls.contains(oldUrl))
620 	{
621 		m_urls[oldUrl].removeAll(entry);
622 
623 		if (m_urls[oldUrl].isEmpty())
624 		{
625 			m_urls.remove(oldUrl);
626 		}
627 	}
628 
629 	if (!newUrl.isEmpty())
630 	{
631 		if (!m_urls.contains(newUrl))
632 		{
633 			m_urls[newUrl] = QVector<Entry*>();
634 		}
635 
636 		m_urls[newUrl].append(entry);
637 	}
638 }
639 
addEntry(EntryType type,const QMap<int,QVariant> & metaData,Entry * parent,int index)640 FeedsModel::Entry* FeedsModel::addEntry(EntryType type, const QMap<int, QVariant> &metaData, Entry *parent, int index)
641 {
642 	Entry *entry(new Entry());
643 
644 	if (!parent)
645 	{
646 		parent = m_rootEntry;
647 	}
648 
649 	parent->insertRow(((index < 0) ? parent->rowCount() : index), entry);
650 
651 	if (type == FeedEntry)
652 	{
653 		entry->setDropEnabled(false);
654 	}
655 
656 	if (type == FolderEntry || type == FeedEntry)
657 	{
658 		QMap<int, QVariant>::const_iterator iterator;
659 
660 		for (iterator = metaData.begin(); iterator != metaData.end(); ++iterator)
661 		{
662 			setData(entry->index(), iterator.value(), iterator.key());
663 		}
664 
665 		createIdentifier(entry);
666 
667 		if (type == FeedEntry)
668 		{
669 			const QUrl url(metaData.value(UrlRole).toUrl());
670 
671 			if (!url.isEmpty())
672 			{
673 				handleUrlChanged(entry, url);
674 			}
675 
676 			entry->setFlags(entry->flags() | Qt::ItemNeverHasChildren);
677 		}
678 	}
679 
680 	entry->setData(type, TypeRole);
681 
682 	emit entryAdded(entry);
683 	emit modelModified();
684 
685 	return entry;
686 }
687 
addEntry(Feed * feed,Entry * parent,int index)688 FeedsModel::Entry* FeedsModel::addEntry(Feed *feed, Entry *parent, int index)
689 {
690 	if (!parent)
691 	{
692 		parent = m_rootEntry;
693 	}
694 
695 	Entry *entry(new Entry(feed));
696 	entry->setData(FeedEntry, TypeRole);
697 	entry->setDropEnabled(false);
698 
699 	createIdentifier(entry);
700 
701 	parent->insertRow(((index < 0) ? parent->rowCount() : index), entry);
702 
703 	const QUrl url(feed->getUrl());
704 
705 	if (!url.isEmpty())
706 	{
707 		handleUrlChanged(entry, url);
708 	}
709 
710 	entry->setFlags(entry->flags() | Qt::ItemNeverHasChildren);
711 
712 	emit entryAdded(entry);
713 	emit modelModified();
714 
715 	return entry;
716 }
717 
getEntry(const QModelIndex & index) const718 FeedsModel::Entry* FeedsModel::getEntry(const QModelIndex &index) const
719 {
720 	Entry *entry(static_cast<Entry*>(itemFromIndex(index)));
721 
722 	if (entry)
723 	{
724 		return entry;
725 	}
726 
727 	return getEntry(index.data(IdentifierRole).toULongLong());
728 }
729 
getEntry(quint64 identifier) const730 FeedsModel::Entry* FeedsModel::getEntry(quint64 identifier) const
731 {
732 	if (identifier == 0)
733 	{
734 		return m_rootEntry;
735 	}
736 
737 	if (m_identifiers.contains(identifier))
738 	{
739 		return m_identifiers[identifier];
740 	}
741 
742 	return nullptr;
743 }
744 
getRootEntry() const745 FeedsModel::Entry* FeedsModel::getRootEntry() const
746 {
747 	return m_rootEntry;
748 }
749 
getTrashEntry() const750 FeedsModel::Entry* FeedsModel::getTrashEntry() const
751 {
752 	return m_trashEntry;
753 }
754 
mimeData(const QModelIndexList & indexes) const755 QMimeData* FeedsModel::mimeData(const QModelIndexList &indexes) const
756 {
757 	QMimeData *mimeData(new QMimeData());
758 	QStringList texts;
759 	texts.reserve(indexes.count());
760 
761 	QList<QUrl> urls;
762 	urls.reserve(indexes.count());
763 
764 	if (indexes.count() == 1)
765 	{
766 		mimeData->setProperty("x-item-index", indexes.at(0));
767 	}
768 
769 	for (int i = 0; i < indexes.count(); ++i)
770 	{
771 		if (indexes.at(i).isValid() && static_cast<EntryType>(indexes.at(i).data(TypeRole).toInt()) == FeedEntry)
772 		{
773 			texts.append(indexes.at(i).data(UrlRole).toString());
774 			urls.append(indexes.at(i).data(UrlRole).toUrl());
775 		}
776 	}
777 
778 	mimeData->setText(texts.join(QLatin1String(", ")));
779 	mimeData->setUrls(urls);
780 
781 	return mimeData;
782 }
783 
mimeTypes() const784 QStringList FeedsModel::mimeTypes() const
785 {
786 	return {QLatin1String("text/uri-list")};
787 }
788 
getEntries(const QUrl & url) const789 QVector<FeedsModel::Entry*> FeedsModel::getEntries(const QUrl &url) const
790 {
791 	const QUrl normalizedUrl(Utils::normalizeUrl(url));
792 	QVector<FeedsModel::Entry*> entrys;
793 
794 	if (m_urls.contains(url))
795 	{
796 		entrys = m_urls[url];
797 	}
798 
799 	if (url != normalizedUrl && m_urls.contains(normalizedUrl))
800 	{
801 		entrys.append(m_urls[normalizedUrl]);
802 	}
803 
804 	return entrys;
805 }
806 
moveEntry(Entry * entry,Entry * newParent,int newRow)807 bool FeedsModel::moveEntry(Entry *entry, Entry *newParent, int newRow)
808 {
809 	if (!entry || !newParent || entry == newParent || entry->isAncestorOf(newParent))
810 	{
811 		return false;
812 	}
813 
814 	Entry *previousParent(static_cast<Entry*>(entry->parent()));
815 
816 	if (!previousParent)
817 	{
818 		if (newRow < 0)
819 		{
820 			newParent->appendRow(entry);
821 		}
822 		else
823 		{
824 			newParent->insertRow(newRow, entry);
825 		}
826 
827 		emit modelModified();
828 
829 		return true;
830 	}
831 
832 	const int previousRow(entry->row());
833 
834 	if (newRow < 0)
835 	{
836 		newParent->appendRow(entry->parent()->takeRow(entry->row()));
837 
838 		emit entryMoved(entry, previousParent, previousRow);
839 		emit modelModified();
840 
841 		return true;
842 	}
843 
844 	int targetRow(newRow);
845 
846 	if (entry->parent() == newParent && entry->row() < newRow)
847 	{
848 		--targetRow;
849 	}
850 
851 	newParent->insertRow(targetRow, entry->parent()->takeRow(entry->row()));
852 
853 	emit entryMoved(entry, previousParent, previousRow);
854 	emit modelModified();
855 
856 	return true;
857 }
858 
canDropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent) const859 bool FeedsModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
860 {
861 	const QModelIndex index(data->property("x-item-index").toModelIndex());
862 
863 	if (index.isValid())
864 	{
865 		const Entry *entry(getEntry(index));
866 		Entry *newParent(getEntry(parent));
867 
868 		return (entry && newParent && entry != newParent && !entry->isAncestorOf(newParent));
869 	}
870 
871 	return QStandardItemModel::canDropMimeData(data, action, row, column, parent);
872 }
873 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)874 bool FeedsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
875 {
876 	const EntryType type(static_cast<EntryType>(parent.data(TypeRole).toInt()));
877 
878 	if (type == FolderEntry || type == RootEntry || type == TrashEntry)
879 	{
880 		const QModelIndex index(data->property("x-item-index").toModelIndex());
881 
882 		if (index.isValid())
883 		{
884 			return moveEntry(getEntry(index), getEntry(parent), row);
885 		}
886 
887 		if (data->hasUrls())
888 		{
889 			const QVector<QUrl> urls(Utils::extractUrls(data));
890 
891 			for (int i = 0; i < urls.count(); ++i)
892 			{
893 				addEntry(FeedEntry, {{UrlRole, urls.at(i)}, {TitleRole, (data->property("x-url-title").toString().isEmpty() ? urls.at(i).toString() : data->property("x-url-title").toString())}}, getEntry(parent), row);
894 			}
895 
896 			return true;
897 		}
898 
899 		return QStandardItemModel::dropMimeData(data, action, row, column, parent);
900 	}
901 
902 	return false;
903 }
904 
save(const QString & path) const905 bool FeedsModel::save(const QString &path) const
906 {
907 	if (SessionsManager::isReadOnly())
908 	{
909 		return false;
910 	}
911 
912 	QSaveFile file(path);
913 
914 	if (!file.open(QIODevice::WriteOnly))
915 	{
916 		return false;
917 	}
918 
919 	QXmlStreamWriter writer(&file);
920 	writer.setAutoFormatting(true);
921 	writer.setAutoFormattingIndent(-1);
922 	writer.writeStartDocument();
923 	writer.writeStartElement(QLatin1String("opml"));
924 	writer.writeAttribute(QLatin1String("version"), QLatin1String("1.0"));
925 	writer.writeStartElement(QLatin1String("head"));
926 	writer.writeTextElement(QLatin1String("title"), QLatin1String("Newsfeeds exported from Otter Browser ") + Application::getFullVersion());
927 	writer.writeEndElement();
928 	writer.writeStartElement(QLatin1String("body"));
929 
930 	for (int i = 0; i < m_rootEntry->rowCount(); ++i)
931 	{
932 		writeEntry(&writer, static_cast<Entry*>(m_rootEntry->child(i, 0)));
933 	}
934 
935 	writer.writeEndElement();
936 	writer.writeEndDocument();
937 
938 	return file.commit();
939 }
940 
setData(const QModelIndex & index,const QVariant & value,int role)941 bool FeedsModel::setData(const QModelIndex &index, const QVariant &value, int role)
942 {
943 	Entry *entry(getEntry(index));
944 
945 	if (!entry)
946 	{
947 		return QStandardItemModel::setData(index, value, role);
948 	}
949 
950 	if (role == UrlRole && value.toUrl() != index.data(UrlRole).toUrl())
951 	{
952 		handleUrlChanged(entry, Utils::normalizeUrl(value.toUrl()), Utils::normalizeUrl(index.data(UrlRole).toUrl()));
953 	}
954 
955 	entry->setData(value, role);
956 
957 	switch (role)
958 	{
959 		case TitleRole:
960 		case UrlRole:
961 		case DescriptionRole:
962 		case IdentifierRole:
963 		case TypeRole:
964 			emit entryModified(entry);
965 			emit modelModified();
966 
967 			break;
968 		default:
969 			break;
970 	}
971 
972 	return true;
973 }
974 
hasFeed(const QUrl & url) const975 bool FeedsModel::hasFeed(const QUrl &url) const
976 {
977 	return (m_urls.contains(Utils::normalizeUrl(url)) || m_urls.contains(url));
978 }
979 
980 }
981