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